C++多线程环境下的单例类对象创建

本文介绍了在多线程环境下使用C++的不同方法创建单例对象,包括使用局部静态变量、无锁编程、std::unique_ptr和std::call_once或std::atomic_flag实现线程安全的单例。
摘要由CSDN通过智能技术生成

使用C++无锁编程实现多线程下的单例模式

贺志国
2023.8.1

在多线程环境下创建一个类的单例对象,要比单线程环境下要复杂很多。下面介绍在多线程环境下实现单例模式的几种方法。

一、尺寸较小的类单例对象创建

如果待创建的单例类SingletonForMultithread内包含的成员变量较少,整个类占用的内存空间较小,则可使用局部静态变量来创建单例对象。C++ 11标准保证在多线程环境下,在第一个线程未完成静态对象的构建前,其他线程必须等待其完成,因此是线程安全的。如果类的尺寸较大,静态变量存储栈区无法容纳该类的单例对象,则禁止使用该方法。例如:64位Linux系统默认栈的最大空间为8 MB,64位Windows系统默认栈的最大空间为1 MB,当待创建的单例对象尺寸接近或超过上述栈的默认存储空间时,如使用该方法创建则会导致程序崩溃。示例代码如下所示:

class SmallSingletonForMultithread {
 public:
  static SmallSingletonForMultithread& GetInstance() {
    static SmallSingletonForMultithread instance;
    return instance;
  }
  
 private:
  SmallSingletonForMultithread() = default;
  ~SmallSingletonForMultithread() = default;

  SmallSingletonForMultithread(const SmallSingletonForMultithread&) = delete;
  SmallSingletonForMultithread& operator=(const SmallSingletonForMultithread&) = delete;
  SmallSingletonForMultithread(SmallSingletonForMultithread&&) = delete;
  SmallSingletonForMultithread& operator=(SmallSingletonForMultithread&&) = delete;
};

二、尺寸较大的类单例对象创建(使用无锁编程来实现)

在实际工作中,由于某些单例类的尺寸较大,静态变量存储栈区无法容纳该单例对象,因此无法使用上述方法来创建单例对象,这时需要使用new在堆区动态创建单例对象。为了避免多线程环境下对于单例对象的抢夺,可使用C++无锁编程来实现。在程序结束时使用atexit(DestoryInstance)来删除单例对象,示例代码如下所示:

#include <atomic>
#include <cassert>
#include <memory>
#include <thread>
#include <vector>

namespace {
constexpr size_t kThreadNum = 2000;
}

class SingletonForMultithread {
 public:
  static SingletonForMultithread* GetInstance() {
    if (!instance_.load(std::memory_order_acquire)) {
      auto* new_ptr = new SingletonForMultithread;
      SingletonForMultithread* old_ptr = nullptr;
      if (!instance_.compare_exchange_strong(old_ptr, new_ptr,
                                             std::memory_order_release,
                                             std::memory_order_relaxed)) {
        // If the CAS operation fails, another thread has created a singleton
        // object, and it's necessary to delete the temporary object created by
        // the current thread.
        delete new_ptr;
        new_ptr = nullptr;
      }
      
      // When the program exits, the function to delete the singleton object 
      // is called.
      atexit(DestoryInstance);
    }

    return instance_.load(std::memory_order_relaxed);
  }

  static void DestoryInstance() {
    if (instance_.load(std::memory_order_acquire)) {
      auto* old_ptr = instance_.load(std::memory_order_relaxed);
      SingletonForMultithread* new_ptr = nullptr;
      if (instance_.compare_exchange_strong(old_ptr, new_ptr,
                                            std::memory_order_release,
                                            std::memory_order_relaxed)) {
        // If the CAS operation succeeds, the current thread obtains the
        // original object and can safely delete it.
        delete old_ptr;
        old_ptr = nullptr;
      }
    }
  }

 private:
  SingletonForMultithread() = default;
  ~SingletonForMultithread() = default;

  SingletonForMultithread(const SingletonForMultithread&) = delete;
  SingletonForMultithread& operator=(const SingletonForMultithread&) = delete;
  SingletonForMultithread(SingletonForMultithread&&) = delete;
  SingletonForMultithread& operator=(SingletonForMultithread&&) = delete;

 private:
  static std::atomic<SingletonForMultithread*> instance_;
};

// Static member variable initialization
std::atomic<SingletonForMultithread*> SingletonForMultithread::instance_;

int main() {
  std::vector<std::thread> customers;
  for (size_t i = 0; i < kThreadNum; ++i) {
    customers.emplace_back(&SingletonForMultithread::GetInstance);
  }
  for (auto& cutomer : customers) {
    cutomer.join();
  }

  auto* singleton = SingletonForMultithread::GetInstance();
  assert(singleton != nullptr);

  // singleton->DestoryInstance();

  return 0;
}

编译指令如下:

g++ -g -Wall -std=c++14  -O3 singleton_test.cpp -pthread -o singleton_test 

三、尺寸较大的类单例对象创建(使用std::unique_ptr<T>std::call_once实现)

C++ 11还支持使用std::call_once函数来确保单例对象的构造只发生一次,注意本示例中使用了智能指针std::unique_ptr<T>来管理单例对象的生命周期,示例代码如下:

#include <cassert>
#include <memory>
#include <mutex>

class SingletonForMultithread {
 public:
  ~SingletonForMultithread() = default;

  static SingletonForMultithread* GetInstance() {
    static std::unique_ptr<SingletonForMultithread> instance;
    static std::once_flag only_once;

    std::call_once(only_once,
                   []() { instance.reset(new (std::nothrow) SingletonForMultithread); });

    return instance.get();
  }

 private:
  SingletonForMultithread() = default;

  SingletonForMultithread(const SingletonForMultithread&) = delete;
  SingletonForMultithread& operator=(const SingletonForMultithread&) = delete;
  SingletonForMultithread(SingletonForMultithread&&) = delete;
  SingletonForMultithread& operator=(SingletonForMultithread&&) = delete;
};

int main() {
  auto* singleton = SingletonForMultithread::GetInstance();
  assert(singleton != nullptr);

  return 0;
}

但我在Ubuntu 20.04系统上使用GCC 9.4.0似乎无法正常完成任务,会抛出异常,产生core dump,原因暂不详。
gcc
core dump

8月10日补充说明:

经同事路遥提示,出现core dump的根本原因是链接pthread库的指令不正确,我最开始使用的编译指令是:

g++ -g -Wall -std=c++14  -O3 singleton_test.cpp -lpthread -o singleton_test 

以上命令中,-lpthread的使用不正确,具体原因参见该博客:编译时-pthread和-lpthread之间的区别。正确的编译指令如下:

g++ -g -Wall -std=c++14  -O3 singleton_test.cpp -pthread -o singleton_test 

使用上述命令编译生成的程序能够正常运行。

四、尺寸较大的类单例对象创建(使用std::unique_ptr<T>std::atomic_flag实现)

还可使用std::atomic_flag替换std::call_once来完成任务,基本思想如下:首先定义一个静态的无锁标志变量std::atomic_flag start_flag,并将其初始值设置为ATOMIC_FLAG_INIT。第一次调用start_flag.test_and_set(std::memory_order_relaxed)函数时,由于start_flag的状态是ATOMIC_FLAG_INIT,该函数返回false,于是可调用instance.reset(new SingletonForMultithread)创建单例对象。第二次直至第N次调用start_flag.test_and_set(std::memory_order_relaxed)函数时,因为start_flag的状态已被设置,该函数返回true,创建单例对象的语句instance.reset(new SingletonForMultithread)永远不会被再次执行,这就达到了只创建一次的目的。同时,因为使用静态的智能指针变量std::unique_ptr<SingletonForMultithread> instance来管理单例对象,于是不再需要显式地回收内存,只要程序结束,静态变量自动清除,智能指针对象instance会在其析构函数中释放内存。

由于new运算符创建单例对象可能耗时较长,为了避免其他线程在单例对象创建到一半的过程中读取到不完整的对象,导致未定义的行为,我们使用另一个原子变量std::atomic<bool> finished来确保创建动作已正确完成,不选用另一个无锁标志变量std::atomic_flag的原因是,该类在C++ 20标准前未提供单独的测试函数testfinished.store(true, std::memory_order_release);while (!finished.load(std::memory_order_acquire))的内存顺序,实现了synchronizes-withhappens-before关系,保证在while (!finished.load(std::memory_order_acquire))成功时,instance.reset(new SingletonForMultithread);必定执行完毕,单例对象的创建是完整的。

完整的示例代码如下:

#include <atomic>
#include <cassert>
#include <memory>
#include <mutex>
#include <thread>
#include <vector>

using namespace std::chrono_literals;

namespace {
constexpr size_t kThreadNum = 2000;
}

class SingletonForMultithread {
 public:
  ~SingletonForMultithread() = default;

  static SingletonForMultithread* GetInstance() {
    static std::unique_ptr<SingletonForMultithread> instance;
    static std::atomic_flag start_flag = ATOMIC_FLAG_INIT;
    static std::atomic<bool> finished(false);

    if (!finished.load(std::memory_order_acquire)) {
      // 'start_flag.test_and_set' is a completed or incomplete atomic
      // operation. It is a one-time read-modify-write operation. If it
      // completes, the modified value must have been written to the memory.
      // Because its result does not need to be synchronized with other threads,
      // the simplest memory order is used: memory_order_relaxed
      if (!start_flag.test_and_set(std::memory_order_relaxed)) {
        // The object created by the `new` operator may be relatively large and
        // time-consuming, therefore another atomic variable 'finished' is used
        // to ensure that other threads read a fully constructed singleton
        // object. Do not consider using another `std::atomic_flag`. Because it
        // doesn't provide a separate `test` function before the C++20 standard.
        instance.reset(new (std::nothrow) SingletonForMultithread);
        finished.store(true, std::memory_order_release);
      } else {
        // Wait in a loop until the singleton object is fully created, using
        // `std::this_thread::yield()` to save CPU resources.
        while (!finished.load(std::memory_order_acquire)) {
          std::this_thread::yield();
        }
      }
    }

    return instance.get();
  }

 private:
  SingletonForMultithread() {
    // Simulate a constructor that takes a relative long time.
    std::this_thread::sleep_for(10ms);
  }

  SingletonForMultithread(const SingletonForMultithread&) = delete;
  SingletonForMultithread& operator=(const SingletonForMultithread&) = delete;
  SingletonForMultithread(SingletonForMultithread&&) = delete;
  SingletonForMultithread& operator=(SingletonForMultithread&&) = delete;
};

int main() {
  std::vector<std::thread> customers;
  for (size_t i = 0; i < kThreadNum; ++i) {
    customers.emplace_back(&SingletonForMultithread::GetInstance);
  }
  for (auto& cutomer : customers) {
    cutomer.join();
  }

  auto* singleton = SingletonForMultithread::GetInstance();
  assert(singleton != nullptr);

  return 0;
}

原子变量之间的内存顺序示意图如下:

memory order

测试代码中,我们特意在构造函数中添加了让当前线程休眠10 ms的代码:std::this_thread::sleep_for(10ms);, 以此来模拟一个耗时较长的构造过程。另外,通过2000个线程来同时访问SingletonForMultithread::GetInstance(),通过下图的调试界面可看出,在SingletonForMultithread()比较漫长的构建过程中,确实有多个线程闯入,这时等待构建过程完成的代码:

    while (!finished.load(std::memory_order_acquire)) {
      std::this_thread::yield();
    }

确实发挥出了应有的拦截作用,整个过程是线程安全的。

debug
SingletonForMultithread* GetInstance()函数在ARM 64平台下使用GCC 9.4带-O3优化选项生成的汇编代码如下:

SingletonForMultithread::GetInstance():
        push    {r4, r5, r6, lr}
        ldr     r4, .L35
        ldrb    r5, [r4]        @ zero_extendqisi2
        bl      __sync_synchronize
        tst     r5, #1
        beq     .L31
.L10:
        ldrb    r5, [r4, #8]    @ zero_extendqisi2
        bl      __sync_synchronize
        cmp     r5, #0
        beq     .L32
.L19:
        ldr     r0, [r4, #4]
        pop     {r4, r5, r6, pc}
.L31:
        mov     r0, r4
        bl      __cxa_guard_acquire
        cmp     r0, #0
        beq     .L10
        ldr     r2, .L35+4
        ldr     r1, .L35+8
        add     r0, r4, #4
        bl      __aeabi_atexit
        mov     r0, r4
        bl      __cxa_guard_release
        ldrb    r5, [r4, #8]    @ zero_extendqisi2
        bl      __sync_synchronize
        cmp     r5, #0
        bne     .L19
.L32:
        mov     r6, r4
        ldrb    r3, [r6, #12]!  @ zero_extendqisi2
.L13:
        lsl     r1, r3, #24
        asr     r1, r1, #24
        mov     r2, #1
        mov     r0, r6
        mov     r5, r3
        bl      __sync_val_compare_and_swap_1
        and     r2, r5, #255
        mov     r3, r0
        and     r1, r3, #255
        cmp     r1, r2
        bne     .L13
        cmp     r2, #0
        bne     .L14
        b       .L33
.L34:
        bl      __gthrw_sched_yield()
.L14:
        ldrb    r5, [r4, #8]    @ zero_extendqisi2
        bl      __sync_synchronize
        cmp     r5, #0
        beq     .L34
        ldr     r0, [r4, #4]
        pop     {r4, r5, r6, pc}
.L33:
        ldr     r1, .L35+12
        mov     r0, #1
        bl      operator new(unsigned int, std::nothrow_t const&)
        subs    r5, r0, #0
        beq     .L18
        bl      SingletonForMultithread::SingletonForMultithread() [complete object constructor]
.L18:
        ldr     r0, [r4, #4]
        str     r5, [r4, #4]
        cmp     r0, #0
        beq     .L17
        mov     r1, #1
        bl      operator delete(void*, unsigned int)
.L17:
        bl      __sync_synchronize
        mov     r3, #1
        strb    r3, [r4, #8]
        ldr     r0, [r4, #4]
        pop     {r4, r5, r6, pc}
        ldr     r1, .L35+12
        mov     r0, r5
        bl      operator delete(void*, std::nothrow_t const&)
        bl      __cxa_end_cleanup
.L35:
        .word   .LANCHOR0
        .word   __dso_handle
        .word   _ZNSt10unique_ptrI23SingletonForMultithreadSt14default_deleteIS0_EED1Ev
        .word   _ZSt7nothrow

说明:r0-r15是ARMv7 32位架构下的通用寄存器,每个寄存器的空间为4字节(32位),其中r13又称为sp(栈顶指针),r14 又称为lr(链接寄存器,即当前调用结束后的返回地址),r15又称为pc(程序计数器);在ARMv8 64位架构下,通用寄存器为x0-x30,每个寄存器的空间为8字节(64位),w0-w30分别表示对应的x0-x30的低32位寄存器。在ARMv8 64位架构下,x0 = r0, x1 = r1, …, x15 = r15

上述代码首先从.L35加载内容到寄存器r4,再将其转存到寄存器r5, 然后加载 r5 寄存器中的一个字节,这个字节表示单例对象是否已经被创建。然后使用 __sync_synchronize 函数进行同步,确保前面的加载操作完成。接下来,代码通过比较 r5 是否为 1(已创建)来决定是继续执行还是等待。如果已经创建,则直接跳到 .L19,否则继续执行。在 .L31 标签处,代码调用 __cxa_guard_acquire 函数来获取锁,确保只有一个线程可以进入临界区。如果没有获取到锁,则跳回 .L10,继续尝试获取锁。在获取到锁之后,代码进行一系列操作,包括调用 __aeabi_atexit 函数注册析构函数,调用 __cxa_guard_release 函数释放锁,并将单例对象的地址存储在 r0 中。然后,代码再次加载 r5 寄存器中的一个字节,进行同步,并检查 r5 是否为 0。如果为 0,则跳转到 .L32 标签处,继续尝试获取锁。否则,继续执行。在 .L32 标签处,代码使用 __sync_val_compare_and_swap_1 函数来进行原子比较和交换操作,以确保只有一个线程可以成功创建单例对象。之后,代码进行一系列操作,包括调用构造函数和析构函数,分配和释放内存等。最后,代码进行一系列的清理操作,并将单例对象的地址存储在 r0 中,最终返回该地址。

编译命令如下:

g++ -g -Wall -std=c++14  -O3 singleton_test.cpp -pthread -o singleton_test 

如果使用CMake编译,配置文件CMakeLists.txt如下:

cmake_minimum_required(VERSION 3.0.0)
project(singleton_test VERSION 0.1.0)
set(CMAKE_CXX_STANDARD 14)

# If the debug option is not given, the program will not have debugging information.
SET(CMAKE_BUILD_TYPE "Debug")

add_executable(${PROJECT_NAME} ${PROJECT_NAME}.cpp)

find_package(Threads REQUIRED)
set(THREADS_PREFER_PTHREAD_FLAG ON)
# target_link_libraries(${PROJECT_NAME} ${CMAKE_THREAD_LIBS_INIT})
target_link_libraries(${PROJECT_NAME} Threads::Threads)

include(CTest)
enable_testing()
set(CPACK_PROJECT_NAME ${PROJECT_NAME})
set(CPACK_PROJECT_VERSION ${PROJECT_VERSION})
include(CPack)

编译指令为:

# Do it only once
mkdir build
cd build
cmake .. && make
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值