Double-checkedlockingmode(DCLP)在无锁编程(lock-freeprogramming)中经常被讨论,直到2004年,JAVA才提供可靠的双重检查锁定实施。在C++11之前,C++没有提供此模式的可移植且可靠的实现。随着各种语言实现中双重检查锁定模式的缺点暴露出来,人们开始研究如何安全可靠地实现它。2000年,一个Java高性能研究小组发表声明《双重检查锁定可能导致锁定无效》。2004年,ScottMeyers和AndreiAlexandrescu共同发表了一篇名为《C++实现双重检查锁定存在严重缺陷》的论文。这两篇论文着重介绍了什么是双重检查锁定(DCLP),以及双重检查锁定的意义,目前的语言实现存在很多不足。如今,JAVA为了安全的实现双重检查锁,修改了内存模型,引入了关键字volatile。同时,C++构建了全新的内存模型和原子操作库(atomic),使得不同的编译器更容易实现双重检查锁定(DCLP)。为了在早期的C\C++编译器中实现DCLP,我在今年早些时候发布的C++11中引入了一个名为Mintomic的库。在过去的一段时间里,我一直专注于用C++实现DCLP的研究。什么是双重检查锁定?如果想在多线程编程中安全的使用Singleton,最简单的方法就是在访问的时候加锁。这样,假设两个线程同时调用了Singleton::getInstance方法,其中一个线程负责创建一个单例:Singleton*Singleton::getInstance(){Locklock;//基于作用域的锁,当functionreturnsif(m_instance==NULL){m_instance=newSingleton;}returnm_instance;}用这个方法是可行的,但是当singleton被创建之后,其实就不需要再加锁了。虽然加锁不一定会导致性能低下,但在重负载下也可能导致响应变慢。使用双重检查锁定模式可以避免在创建单例对象后进行不必要的锁定。但是,实现有点复杂。Meyers-Alexandrescu论文中也对其进行了描述。论文中提出了几个有缺陷的实现,并一一解释为什么这个实现有问题。在论文最后的第12页,给出了一个可靠的实现,它依赖于标准中未指定的内存栅栏技术。Singleton*Singleton::getInstance(){Singleton*tmp=m_instance;...//insertmemorybarrierif(tmp==NULL){Locklock;tmp=m_instance;if(tmp==NULL){tmp=newSingleton;...//insertmemorybarrierm_instance=tmp;}}returntmp;}这里我们可以看到,和模式名一样,在代码中实现了双重验证。当m_instance指针为NULL时,我们进行锁定。这个过程在***创建对象的线程中是可见的。在create-thread内部构造块中,再次检查m_instance以确保该线程只创建了一个对象副本。这就是double-checkedlocking的实现,但是在高亮代码行中还是缺少内存栅栏技术来保证。在写这篇文章的时候,C/C++编译器还没有统一这种实现,而在C++11标准中,已经对这种情况下的实现进行了改进和统一。C++11中获取和释放内存栅栏在C++11中,可以获取和释放内存栅栏来实现上述功能(如何获取和释放内存栅栏在我之前的博文中有介绍)。为了使您的代码在各种C++实现中更具可移植性,您应该使用C++11中新的原子类型来包装您的m_instance指针,这使得对m_instance的操作成为原子操作。以下代码演示了如何使用内存栅栏,请注意代码中高亮部分:std::atomicSingleton::m_instance;std::mutexSingleton::m_mutex;Singleton*Singleton::getInstance(){Singleton*tmp=m_instance.load(std::memory_order_relaxed);std::atomic_thread_fence(std::memory_order_acquire);//编者注:if(tmp==nullptr){std::lock_guardlock(m_mutex);tmp=m_instance.load(std::memory_order_relaxed);if(tmp==nullptr){tmp=newSingleton;std::atomic_thread_fence(std::memory_order_release);//编者注:作者提醒的m_instance.store(tmp,std::memory_order_relaxed);}}returntmp;}以上代码在多核系统中仍然可以正常运行,这是因为内存栅栏技术建立了一种“同步-与”关系(synchronizes-with).Singleton::m_instance充当保护变量,而单例本身充当有效载荷。其他有缺陷的双重检查锁定实现缺乏这种机制的保证:在没有“同步-与”关系保证的情况下,第一个创建线程的写操作,具体是在其构造函数中,可以被其他线程感知,即m_instance指针可以被其他线程访问!在单例线程中创建锁也没有效果,因为锁对其他线程不可见,导致在某些情况下会多次执行对象的创建。如果你想了解内存栅栏技术如何可靠地实现双重检查锁定的内部原理,在我的上一篇文章(上一篇)中有一些背景信息,以及上一篇博客中的一些相关内容。#p#使用Mintomic内存栅栏Mintomic是一个小型c库,它提供了C++11原子库中一些功能函数的子集,包括获取和释放内存栅栏,它可以在早期的编译器上工作。Mintomic依赖于类似于C++11的内存模型——具体来说,没有使用凭空存储——一种在早期编译器中没有实现的技术,如果没有C++11,这可能是我们能做的最好的实现标准情况下。从我多年的C++多线程开发经验来看,Out-of-thin-air存储并不流行,大多数编译器都会避免实现。下面的代码演示了如何使用Mintomic的acquire和releasememoryfence机制来实现double-checkedlocking,和上面的例子基本类似:mint_atomicPtr_tSingleton::m_instance={0};mint_mutex_tSingleton::m_mutex;Singleton*Singleton::getInstance(){Singleton*tmp=(Singleton*)mint_load_ptr_relaxed(&m_instance);mint_thread_fence_acquire();if(tmp==NULL){mint_mutex_lock(&m_mutex);tmp=(Singleton*)mint_load_ptr_relaxed(&m_instance);if(tmp==NULL){tmp=newSingleton;mint_thread_fence_release();mint_store_ptr_relaxed(&m_instance,tmp);}mint_mutex_unlock(&m_mutex);}returntmp;}为了实现获取和释放内存栅栏,Mintomic将尝试在其支持的编译器平台代码上生成最高效的机器.例如,下面的汇编代码来自使用PowerPC处理器的Xbox360。在此平台上,内联的lwsync关键字是用于获取和释放内存栅栏的优化指令。上述使用C++11标准库编译的示例应该在PowerPC处理器上生成相同的汇编代码(理想情况下)。但是,我无法在PowerPC下编译C++11来验证这一点。使用C++11低级指令顺序约束在C++11中使用内存屏障锁定技术可以轻松实现双重检查锁定。它还保证在当今流行的多核系统上生成优化的机器代码(Mintomic也这样做)。但是,这种方法并不常用。C++11中更好的实现是使用保证低级指令执行顺序约束的原子操作。如上图所示,写-释放操作可以与获取-读取操作同步:std::atomicSingleton::m_instance;std::mutexSingleton::m_mutex;Singleton*Singleton::getInstance(){Singleton*tmp=m_instance.load(std::memory_order_acquire);if(tmp==nullptr){std::lock_guardlock(m_mutex);tmp=m_instance.load(std::memory_order_relaxed);if(tmp==nullptr){tmp=newSingleton;m_instance.store(tmp,std::memory_order_release);}}returntmp;}从技术上讲,使用这种形式的无锁同步比独立内存栅栏技术更具限制性低的。上面的操作只是为了防止自身操作的内存排序,但是内存屏障技术阻止了相邻操作的内存排序。然而,对于今天的x86/64、ARMv6/v7和PowerPC处理器架构,为这两种形式生成的机器代码应该是相同的。在我之前的博文中,我展示了C++11低级指令顺序约束使用了ARM7中的dmb指令,这与使用内存栅栏技术生成的汇编代码是一致的。以上两种方法在安腾平台上可能会生成不同的机器码。在Itanium平台上,C++11标准中的load(memory_order_acquire)可以使用单CPU指令:ld.acq,而store(tmp,memory_order_release)使用st.rel即可实现。在ARMv8处理器架构中,也提供了相当于Itanium指令的ldar和stlr指令,但不同的是,这些指令还会导致stlr和后续ldar之间进一步的存储和加载指令被排序。事实上,ARMv8的新指令试图实现C++11标准中的顺序约束原子操作,这将在后面进一步介绍。使用C++顺序一致的原子操作C++11标准提供了一种不同的方式来编写无锁程序(双重检查锁定可以归类为一种无锁编程,因为并非所有线程都会获取锁)。在所有原子操作库方法中使用可选参数std::memory_order可以使所有原子变量成为顺序原子操作(顺序一致)。该方法的默认参数是std::memory_order_seq_cst。使用SequentialConstraint(SC)原子操作库,将保证整个函数执行顺序执行,不会出现datarace(数据竞争)。序列约束(SC)原子操作与Java5版本之后出现的volatile变量非常相似。使用SC原子操作实现双重检查锁的代码如下:和前面的例子一样,高亮的第二行会和第一次创建单例的线程同步操作。std::atomicSingleton::m_instance;std::mutexSingleton::m_mutex;Singleton*Singleton::getInstance(){Singleton*tmp=m_instance.load();if(tmp==nullptr){std::lock_guardlock(m_mutex);tmp=m_instance.load();if(tmp==nullptr){tmp=newSingleton;m_instance.store(tmp);}}returntmp;}顺序约束(SC)原子操作使开发人员更容易预测代码执行的结果。缺点是使用顺序约束(SC)原子操作的库的代码效率低于前面的示例。例如,在x64位机器上,上述代码使用Clang3.3优化生成如下汇编代码:由于使用了顺序约束(SC)原子操作类库,变量m_instance的存储操作使用xchg指令,相当于在内存栅栏上操作。该指令在x64位处理器中属于长周期指令,使用轻量级的mov指令也可以完成操作。然而,这并没有什么效果,因为xchg指令只被单例创建过程调用一次。然而,在PowerPC或ARMv6/v7处理器上编译上述代码会产生更糟糕的汇编操作,有关详细信息,请参阅HerbSutter的演讲(原子武器演讲,第2.00:44:25–00:49:16部分)。#p#使用C++11数据序列依赖原则上面的例子都使用了创建单线程和使用单线程之间的同步和关系。守护的是数据指针的单元素,开销是创建单例内容本身。在这里,我将演示使用数据依赖性的指针防护防御。在使用数据依赖时,上面的例子中使用了read-fetch操作,同样会造成性能消耗。我们可以使用消费指令进一步优化。consume指令非常酷,它在PowerPc处理器上使用lwsync指令,在ARMv7处理器上编译为dmd指令。以后我会写一些文章来描述消费指令和数据依赖的机制。使用C++11静态初始化可能有读者已经知道,在C++11中,可以跳过前面的检查,直接获取线程安全的单例。您只需要使用静态初始化:C++11标准在第6.7.4节中规定:如果指令逻辑进入未初始化的声明变量,则所有并发执行都应等待该变量完成初始化。以上操作由编译器在编译时保证。双重检查锁定可以利用这一点。不保证编译器会使用双重检查锁定,但大多数编译器都会这样做。gcc4.6在ARM处理器上使用-std=c++0x编译选项生成的汇编代码如下:由于单例使用固定地址,编译器会使用一个特殊的防御变量来完成同步。此处注意,在启动变量读取时,dmb指令不用于获取内存栅栏。守卫变量指向单例,因此编译器可以利用数据依赖原则来避免使用dmb指令的开销。__cxa_guard_release指令充当写释放以取消对变量的保护。一旦设置了防护栅栏,就会在读取操作之前执行指令顺序。在这里,与前面的示例一样,对内存排序进行自适应更改。之前的长文主要讲述了C++11标准修复了double-checkedlocking的实现,同时也讲述了一些其他的相关知识。个人认为应该在程序初始化的时候初始化一个单例。使用双重检查锁定可以帮助您将任意数据类型存储在无锁哈希表中。这将在后续文章中进一步阐述。原文链接:http://preshing.com/20130930/double-checked-locking-is-fixed-in-cpp11/翻译链接:http://blog.jobbole.com/52164/