您的位置: 网界网 > 网络学院-技术开发 > 正文

C++11 中的双重检查锁定模式(1)

2014年06月19日 20:38:35 | 作者:佚名 | 来源:51CTO | 查看本文手机版

摘要:在过去。java现在可以为修订内存模型,为thevolatileeyword注入新的语义,使得它尽可然安全实现DCLP.同样地,c+11有一个全 新的内存模型和原子库使得各种各样的便捷式DCLP得以实现。c+11反过来启发Mintomic,一个小型图书馆,我...

标签
C++锁定模式

双重检查锁定模式(DCLP)在无锁编程方面是有点儿臭名昭著案例学术研究的味道。直到2004年,使用java开发并没有安全的方式来实现它。在 c 11之前,使用便捷式c 开发并没有安全的方式来实现它。由于引起人们关注的缺点模式暴露在这些语言之中,人们开始写它。一组高调的java聚集在 一起开发人员并签署了一项声明,题为:“双重检查锁定坏了”。在2004年斯科特 、梅尔斯和安德烈、亚历山发表了一篇文章,题为:“c 与双重检查锁定 的危险”对于DCLP是什么?这两篇文章都是伟大的引物,为什么呢?在当时看来,这些语言都不足以实现它。

在过去。java现在可以为修订内存模型,为thevolatileeyword注入新的语义,使得它尽可然安全实现DCLP.同样地,c 11有一个全 新的内存模型和原子库使得各种各样的便捷式DCLP得以实现。c 11反过来启发Mintomic,一个小型图书馆,我今年早些时候发布的,这使得它尽可 能的实现一些较旧的c/c 编译器以及DCLP.

在这篇文章中,我将重点关注c 实现的DCLP.

什么是双重检查锁定?

假设你有一个类,它实现了著名的Singleton 模式,现在你想让它变得线程安全。显然的一个方法就是通过增加一个锁来保证互斥共享。这样的话,如果有两个线程同时调用了Singleton::getInstance,将只有其中之一会创建这个单例。

  1. Singleton* Singleton::getInstance() { Lock lock; // scope-based lock, released automatically when the function returns if (m_instance == NULL) { 
  2.         m_instance = new Singleton; 
  3.     } return m_instance; 

这是完全合法的方法,但是一旦单例被创建,实际上就不再需要锁了。锁不一定慢,但是在高并发的条件下,不具有很好的伸缩性。

双重检查锁定模式避免了在单例已经存在时候的锁定。不过如Meyers-Alexandrescu的论文所显示的,它并不简单。在那篇论文中,作者描述了几个有缺陷的用C 实现DCLP的尝试,并剖析了每种情况为什么是不安全的。最后,在第12页,他们给出了一个安全的实现,但是它依赖于非指定的,特定平台的内存屏障(memory barriers)。

(译注:内存屏障就是一种干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序)

  1. Singleton* Singleton::getInstance() {  
  2.     Singleton* tmp = m_instance; ... // insert memory barrier if (tmp == NULL) {  
  3.         Lock lock;  
  4.         tmp = m_instance; if (tmp == NULL) {  
  5.             tmp = new Singleton; ... // insert memory barrier m_instance = tmp;  
  6.         }  
  7.     } return tmp;  
  8. }  

这里,我们可以发现双重检查锁定模式是由此得名的:在单例指针m_instance为NULL的时候,我们仅仅使用了一个锁,这个锁使偶然访问到该单例的第一组线程继续下去。而在锁的内部,m_instance被再次检查,这样就只有第一个线程可以创建这个单例了。

这与可运行的实现非常相近。只是在突出显示的几行漏掉了某种内存屏障。在作者写这篇论文的时候,还没有填补此项空白的轻便的C/C 函数。现在,C 11已经有了。

用 C 11 获得与释放屏障

你可以用获得与释放屏障 安全的完成上述实现,在我以前的文章中我已经详细的解释过这个主题。不过,为了让代码真正的具有可移植性,你还必须要将m_instance包装成原子类型,并且用放松的原子操作(译注:即非原子操作)来操作它。这里给出的是结果代码,获取与释放屏障部分高亮了。

  1. std::atomic Singleton::m_instance; 
  2. std::mutex Singleton::m_mutex; 
  3.  
  4. Singleton* Singleton::getInstance() { 
  5.     Singleton* tmp = m_instance.load(std::memory_order_relaxed); std::atomic_thread_fence(std::memory_order_acquire); if (tmp == nullptr) { 
  6.         std::lock_guard lock(m_mutex); 
  7.         tmp = m_instance.load(std::memory_order_relaxed); if (tmp == nullptr) { 
  8.             tmp = new Singleton; std::atomic_thread_fence(std::memory_order_release); m_instance.store(tmp, std::memory_order_relaxed); 
  9.         } 
  10.     } return tmp; 

即使是在多核系统上,它也可以令人信赖的工作,因为内存屏障在创建单例的线程与其后任何跳过这个锁的线程之间,创建了一种同步的关系。Singleton::m_instance充当警卫变量,而单例本身的内容充当有效载荷

所有那些有缺陷的DCLP实现都忽视了这一点:如果没有同步的关系,将无法保证第一个线程的所有写操作——特别是,那些在单例构造器中执行的写操作——可以对第二个线程可见,虽然m_instance指针本身是可见的!第一个线程具有的锁也对此无能为力,因为第二个线程不必获得任何锁,因此它能并发的运行。

如果你想更深入的理解这些屏障为什么以及如何使得DCLP具有可信赖性,在我以前的文章中有一些背景信息,就像这个博客早前的文章一样。

使用 Mintomic 屏障

Mintomic 是一个小型的C语言的库,它提供了C 11原子库的一个功能子集,其中包含有获取与释放屏障,而且它是运行于更老的编译器之上的。Mintomic依赖于这样的假设 ,即C 11的内存模型——特殊的是,其中包括无中生有的存储 ——因为它不被更老的编译器支持,不过这已经是我们不通过C 11能做到的最佳程度了。记住这些东西可是若干年来我们在写多线程C 代码时的环境。无 中生有的存储(Out-of-thin-air stores)已被时间证明是不流行的,而且好的编译器也基本上不会这么做。

这里有一个DCLP的实现,就是用Mintomic来获取与释放屏障的。和前面使用C 11获取和释放屏障的例子比起来,它基本上是等效的。

  1. mint_atomicPtr_t Singleton::m_instance = { 0 }; 
  2. mint_mutex_t Singleton::m_mutex; 
  3.  
  4. Singleton* Singleton::getInstance() { 
  5.     Singleton* tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); mint_thread_fence_acquire(); if (tmp == NULL) { 
  6.         mint_mutex_lock(&m_mutex); 
  7.         tmp = (Singleton*) mint_load_ptr_relaxed(&m_instance); if (tmp == NULL) { 
  8.             tmp = new Singleton; mint_thread_fence_release(); mint_store_ptr_relaxed(&m_instance, tmp); 
  9.         } 
  10.         mint_mutex_unlock(&m_mutex); 
  11.     } return tmp; 

12
[责任编辑:孙可 sun_ke@cnw.com.cn]