DCLP不是线程安全的?

本文阐述了DCLP(Double-Checked Loking Pattern)在多线程环境下的线程安全问题。DCLP用于解决多线程下共享资源的lazy initialization效率问题,但编译器可能颠倒指令顺序,导致多线程下出现未初始化对象。C++/C语言无法约束指令顺序,可通过缓存指针或采用eager initialization解决。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

DCLP不是线程安全的?

hanlray@gmail.com

Revision: 1.0 Date: 2005/09/29


DCLP(Double-Checked Loking Pattern)应该是一种优化模式,用于解决在多线程环境下,为了实现共享资源的lazy initialization,采用通常的锁机制带来的效率问题。其最常见的应用就是用来实现Singleton设计模式,ACE中的Singleton就是这么实现的。它怎么可能不是线程安全的呢?读了Scott Meyers 和 Andrei Alexandrescu的"C++ and the Perils of Double-Checked Locking",恐怕你就再也不敢使用DCLP了。本文是对该文的阐述。

用DCLP实现Singleton的代码一般类似于:

        1  // from the header file
        2  class Singleton {
        3  public:
        4      static Singleton* instance();
        5  ...
        6  private:
        7      static Singleton* pInstance;
        8  };
        9
        10 // from the implementation file
        11 Singleton* Singleton::pInstance = 0;
        12
        13 Singleton* Singleton::instance() {
        14     if (pInstance == 0) {
        15         pInstance = new Singleton;
        16     }
        17     return pInstance;
        18 }

这里pInstance = new Singleton;语句会引发三件事情:

Step 1: 分配内存来容纳一个Singleton对象

Step 2: 在分配好的内存中构造一个Singleton对象

Step 3: 使pInstance指向分配好的内存

如果编译器为该语句产生成的代码是是按照这个顺序,则不会出任何问题;但是编译器可能会颠倒第二步和第三步,生成类似下面的代码:

        Singleton* Singleton::instance() {
            if (pInstance == 0) {
                Lock lock;
                if (pInstance == 0) {
                    pInstance = // Step 3
                    operator new(sizeof(Singleton)); // Step 1
                    new (pInstance) Singleton; // Step 2
                }
            }
            return pInstance;
        }

这样的代码在多线程下就会有问题:假如线程A执行完Step 1和step 3后由于线程调度而被挂起,线程B就会得到一个未初始化过的Singleton对象,这显然是错误的。那么为什么编译器可能会生成这种“错误的”代码呢?编译器的理由:第一,这种代码的行为是一种标准定义的抽象机的observable behavior,亦即这种代码被抽象机认为是正确的。第二,这种代码可能具有更高的效率。 然而问题就出在第一点上:C++的抽象机是单线程的!它根本不知道会有多线程执行的情况!这样的代码在单线程的情况下显然没有问题,因而被抽象机认为是正确的;但这样的代码却不是线程安全的。对C++/C的这种基础问题我们当然无能为力,但如果我们能够对指令的顺序进行约束,DCLP仍然可以是正确的,可是C++/C语言并没有给我们这样的办法;这也是为什么线程包(如Posix Threads)会有部分是用汇编这种移植性不好的语言写的,它们需要用汇编语言来保证一些操作的顺序性,从而使它们在多线程环境下能够正常工作。对此,Peter A. Buhr甚至提出了Are Safe Concurrency Libraries Possible?,针对采用库来为C++加入多线程支持提出了质疑。关于使线程成为C++语言的一部分的提议,看这里

也许最终C++会把线程会引入到语言中,就像Java和.Net CLI所做的那样;但是现在呢?DCLP当然是不能用了,而不用DCLP实现的Singleton,会大大增加锁的开销,因为每次instance函数调用的时候都会带来锁的操作。不过我们可以通过缓存Singleton对象指针来降低这个开销,不写:

            Singleton::instance()->transmogrify(); 
            Singleton::instance()->metamorphose();
            Singleton::instance()->transmute();            
        

而写:

            Singleton* const instance = Singleton::instance(); // cache instance pointer
            instance->transmogrify(); 
            instance->metamorphose();
            instance->transmute();            
        

甚至可以把instace指针存储到线程局部存储中,这样每个线程只会带来一次锁的开销。细想一下,DCLP的这个问题应该是由lazy initialization带来的,lazy initialization真的那么有用吗?eager initialization如何?即在程序开始运行时就初始化好资源。由于程序一开始都是单线程的,所以根本不用考虑同步问题,运行效率当然也最高。

### 关于C++静态局部变量的线程安全性 在C++11标准中,静态局部变量的初始化被设计为线程安全的操作[^1]。这意味着当多个线程同时首次访问某个静态局部变量时,编译器会自动确保只有一个线程能够执行该变量的初始化过程,而其他线程会被阻塞直到初始化完成。 然而,在某些特定情况下,这种默认行为可能会受到影响。例如,通过指定编译选项`-fno-threadsafe-statics`,可以禁用GCC中的线程安全静态初始化机制[^2]。在这种模式下,程序员需要手动实现同步逻辑来保护静态变量的初始化过程。 为了进一步增强程序的安全性和可移植性,开发者可以通过显式的锁机制或其他并发控制技术来管理对共享资源(如静态变量)的访问。以下是几种常见的方法: #### 方法一:使用互斥量 (Mutex) 通过引入`std::mutex`对象并配合`std::lock_guard`或`std::unique_lock`类模板,可以在运行期间强制实施独占访问权限。 ```cpp #include <iostream> #include <thread> #include <mutex> std::mutex mtx; int& getStaticInt() { static int value = []{ std::lock_guard<std::mutex> lock(mtx); return 0; // Initialization logic here. }(); return value; } void workerThread(int id) { std::cout << "Thread " << id << ": Value is " << getStaticInt() << "\n"; } ``` 上述代码片段展示了如何利用Lambda表达式结合RAII风格锁定策略来保障复杂初始化场景下的数据一致性。 #### 方法二:双重检查锁定 (Double-Checked Locking Pattern, DCLP) 尽管DCLP并非总是推荐的最佳实践,但在适当条件下仍可用于减少不必要的性能开销。 ```cpp class Singleton { public: static Singleton* getInstance() { if (!instance_) { // First check without locking std::lock_guard<std::mutex> lock(mutex_); if (!instance_) { // Second check after acquiring the mutex instance_ = new Singleton(); } } return instance_; } private: Singleton() {} ~Singleton(); inline static Singleton* instance_{nullptr}; inline static std::mutex mutex_; }; // Usage example omitted for brevity... ``` 值得注意的是,即使是在支持原子操作语义的新版ISO C++规范之下,正确实现DCLP仍然可能面临诸多挑战;因此除非绝对必要,通常建议优先考虑更简单直观的方式。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值