`

singleton模式深度分析

 
阅读更多

volatile语义及线程安全singleton模式探讨

 

转自:http://www.cnblogs.com/rocketfan/archive/2009/12/05/1617759.html

作者:Scott Meyers and Andrei Alexandrescu   译者: ChengHuige at gmail.com

1.引言 

详尽的讨论了volatile语义以及如何用C++实现线程安全的Singleton模式。 

主要参考Scott Meyers and Andrei Alexandrescu写的“C++ and the Perils of Double-Checked Locking”,这是2004年的文章,以及网上的其他资源。 

其他参考:

 

  • Threads Basics 
        http://www.hpl.hp.com/personal/Hans_Boehm/c++mm/threadsintro.html
  •  The "Double-Checked Locking is Broken" Declaration

    http://www.cs.umd.edu/%7Epugh/java/memoryModel/DoubleCheckedLocking.html 

 

  • 非完美C++ Singleton实现[2]  Ismayday的官方技术博客
        http://hi.baidu.com/ismayday/blog/item/a797d6cae24b0d41f21fe788.html

 

 

  •  C++0x漫谈》系列之:多线程内存模型By 刘未鹏(pongba)
        http://blog.csdn.net/pongba/archive/2007/06/20/1659952.aspx

 

 

  • 一个老外的博客,包括需不需要对int加锁,gcc中的原子变量操作
        http://www.alexonlinux.com/ 

 

 

 

在前面我写了一个使用c++0x STL自带的多线程API的示例http://www.cnblogs.com/rocketfan/archive/2009/12/02/1615093.html

而这里涉及的不是多线程库的API,因为几乎所有的多线程互斥同步问题都可以通过互斥锁mutex和条件变量解决。但是频繁的加锁解锁很耗时,

毕竟用多线程就是要速度,本文涉及的都是相关的与线程库无关的问题。  

2.多线程问题简介

  一个 多线程的程序允许多个线程同时运行,可能要允许它们访问及更新它们共享的变量,每个线程都有其局部变量但是所有的线程都会看到同样的全局变量或者,“static"静态的类成员变量。

   不同的线程可能会运行在同一个处理器上,轮流执行(by interleaving execution).也有可能会运行在不同处理器上,所谓的hardware thread现在很常见。 

 3.volatile变量是什么

这个网上有很多介绍但都没有全面解释。而在“C++ and the Perils of Double-Checked Locking”的附注,作者给出了详细全面的解释。

作者是从volatile的产生讲起的,当时是为了统一的使用相同的地址处理内存地址和IO port地址,所谓memory-mapped I/O (MMIO)。

个人觉得本质都是不同层次存储映射关系吧,类比寄存器和内存。下面都用寄存器和内存解释。

 

  • 读的情况  

 

unsigned int *p = GetMagicAddress();

unsigned int a, b;

a = *p

b = *p; 

 考虑上面的代码,假设GetMagicAddress()是获得内存的地址,那么a = *p, b = *p,会被编译器认为是相同的操作,假设a = *p 使得值缓存在寄存器中,那么为了速度优化,编译器可能会将最后一行的代码换成

b = a;

 这样就不去内存读而是直接去寄存器读但这可能并不是我们想要的,因为如果这期间其他的线程改写了内存中*P的内容呢?

thread1             thread2 

a = *p;

                       *p = 3

b = a  //---------- 编译器优化的结果使得我们读到的并不是最新的p所指的内存的值

 

  • 写的情况

*p = a;

*p = b;

 

向上面的代码,编译器可能会认为*p = a是冗余操作从而去掉它,而这也可能不是我们想要的。

 

volatile的作用 :

volatile exists for specifying special treatment for such locations, specifically:

(1) the content of a volatile variable is “unstable” (can change by means unknown to the compiler),

(2) all writes to volatile data are “observable” so they must be executed religiously, and

(3) all operations on volatile data are executed in the sequence in which they appear in the source code.

  1. 被声明为volatile的变量其内容是不稳定的(unstable),它的值有可能由编译器所不能知晓的情况所改变
  2. 所有对声明为volatile的变量的写操作都是可见的,必须严格执行be executed religiously
  3. 所有对声明为volatile的变量的操作(读写)都必须严格按照源代码的顺序执行
所以上面的第一条确保读正确,第二条确保写正确,第三条确保读写混合的情况正确。JAVA更进一步跨越线程保证上面的条件。而C/C++只对单一线程内部保证。刘未鹏在博客中这么解释:“总而言之,由于C++03标准是单线程的,因此volatile只能保证单线程内语意。对于前面的那个例子,将xy设为volatile只能保证分别在Thread1Thread2中的两个操作是按代码顺序执行的,但并不能保证在Thread2“眼里”的Thread1的两个操作是按代码顺序执行的。也就是说,只能保证两个操作的线程内次序,不能保证它们的线程间次序。一句话,目前的volatile语意是无法保证多线程下的操作的正确性的。” 

 

但是即使是JAVA能够跨越线程保证,仍然是不够的因为volatile和非volatile操作之间的顺序仍然是未定义的,有可能产生问题,考虑下面的代码:

volatile int vi;

void bar(void) {
vi = 1;
foo();
vi = 0;
}

我们一般会认为vi会在调用foo之前设置为1,调用完后会被置为0。然而编译器不会对你保证这一点,它会很高兴的将你的foo()移位,比如跑到vi = 1前面,只要它知道在foo()里不会涉及到其它的volatile操作。所以安全的方法是用栅栏memory barrier例如“asm volatile (”" ::: “memory”)加到foo的前面和后面 来保证严格的执行顺序。

Meyers提到由于上面的原因我们通常会需要加大量的volatile变量,java1.5中的volatile给出了更严格简单的定义,所有对volatile的读操作,都将被确保发生在该语句后面的读写(any memory reference volatile or not)操作的前面。而写操作则保证会发生在该语句前面的读写操作的后面。.NET也定义了跨线程的volatile语意。

4.线程安全的C++ singleton模式

 

  • 最简单的singleton
注:下面有些来自原文,有些来自"非完美Singleton实现",其它参考之3。

 

复制代码
代码
 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  }
复制代码

 

如果在单线程模式那么上面的代码除了instance()可能会有异常安全问题外没有太大问题。但是对于多线程而言,如果两个线程在14判断都读到pInstance = 0同时进入15,那么就会产生两个Singleton对象。而pInstance指向后产生的那一个。

 

  •  加锁保护的singleton
复制代码
代码
1 Singleton* Singleton::instance() {
2     Lock lock// acquire lock (params omitted for simplicity)
3     if (pInstance == 0) {
4         pInstance = new Singleton;
5     }
6     return pInstance;
7 // release lock (via Lock destructor)
复制代码

 

 

 

这样是绝对的安全了,但是由于加锁解锁的代价大,而instance又是可能被频繁调用的函数所以大大影响性能。事实上只要pInstance == 0的时候才可能出现问题,需要加锁,那么有了下面的写法:代码

复制代码
1 Singleton* Singleton::instance() {
2     if (pInstance == 0) {
3         Lock lock// acquire lock (params omitted for simplicity)
4         pInstance = new Singleton;
5     }
6     return pInstance;
7 // release lock (via Lock destructor)
复制代码

 

但是这样是错误的,因为两个线程如果同时在2判断为true,虽然会在3处互斥,但是还是会轮流进入保护区,生成两个Singleton.于是有人想到下面的方法。

 

  • DCL方法(Double Checked  Locking)
这也是ACE中Singleton的实现方法:

 

 代码

复制代码
1 Singleton* Singleton::instance() {
2     if (pInstance == 0) { // 1st test
3         Lock lock;
4         if (pInstance == 0) { // 2nd test
5             pInstance = new Singleton;
6         }
7     }
8     return pInstance;
9 }
复制代码

 

这看上去很完美但是它也是有问题的

在编译器未优化的情况下顺序如下:
1.new operator分配适当的内存;
2.在分配的内存上构造Singleton对象;
3.内存地址赋值给_instance。


但是当编译器优化后执行顺序可能如下:
1.new operator分配适当的内存;
2.内存地址赋值给_instance;

3.在分配的内存上构造Singleton对象。

 编译器优化后的代码看起来像下面这样:

 代码

复制代码
 1 Singleton* Singleton::instance() {
 2     if (pInstance == 0) {
 3         Lock lock;
 4         if (pInstance == 0) {
 5             pInstance = // Step 3
 6               operator new(sizeof(Singleton)); // Step 1
 7             new (pInstance) Singleton; // Step 2
 8          }
 9     }
10     return pInstance;
11 }
复制代码

 

 这样如果一个线程按照编译器优化的顺序执行到5,这时后pInstance就已经非0了,而实际上它所指向的内存上Singleton对象还没有被构造,这个时候有可能另一个线程运行到2,发现pInstance不是0,NULL,于是return pInstance,假设它又用这个指向还未构造对象的指针调用pInstance->doSomeThing() .........:(

 

 未了避免这种情况,于是有了下面的做法

 代码

复制代码
 1 Singleton* Singleton::instance() {
 2      if (pInstance == 0) {
 3          Lock lock;
 4          if (pInstance == 0) {
 5               Singleton* temp = new Singleton; // initialize to temp
 6               pInstance = temp; // assign temp to pInstance
 7          }
 8      }
 9      return pInstance;
10 }
复制代码

 

企图利用一个临时变量,仅当Singleton对象构造完成后才把地址赋给pInstance,然而很不幸,编译器会把你的临时变量视为无用的东西从而优化掉。。。。 

这里插一句为什么用pthread之类的库能够解决问题,保证顺序,因为它们是非语言本身的,往往 强迫编译器产生与之适应的代码,往往会调用系统调用,很多是用汇编实现的。

 

  • 加入volatile
前面讲到volatile的定义,那么这里能否利用volatile呢。上面最后的代码想法不错,只是编译器会捣乱:)那么我们加入volatile来避免编译器的优化,保证语句的顺序执行。不过这里我们考虑这种情况,Singlton带有一个变量x,默认的私有构造函数会将其赋值为5.

 

复制代码
代码
 1 class Singleton {
 2  public:
 3      static Singleton* instance();
 4      ...
 5  private:
 6      static Singleton* volatile pInstance; // volatile added
 7      int x;
 8      Singleton() : x(5) {}
 9  };
10  
11  // from the implementation file
12  Singleton* Singleton::pInstance = 0;
13  
14  Singleton* Singleton::instance() {
15      if (pInstance == 0) {
16          Lock lock;
17          if (pInstance == 0) {
18              Singleton* volatile temp = new Singleton; // volatile added
19              pInstance = temp;
20          }
21      }
22      return pInstance;
23 }
复制代码

 

 

下面考虑编译器inline构造函数和之后的instance()函数的样子

 代码

复制代码
1 if (pInstance == 0) {
2     Lock lock;
3     if (pInstance == 0) {
4         Singleton* volatile temp =
5         static_cast<Singleton*>(operator new(sizeof(Singleton)));
6         temp->= 5// inlined Singleton constructor
7         pInstance = temp;
8     }
9 }
复制代码

 

问题出来了,尽管temp被声明为volatile,但是*temp不是,这意味着temp->x也不是,这意味着编译器可能会把6,7句换位置,从而使得另一个线程得到一个指向x域并未被构造好的Sigleton对象!

解决办法是我们不仅仅声明pInstance为volatile也将*pInstance声明为volatile.如下:

复制代码
代码
 1 class Singleton {
 2 public:
 3     static volatile Singleton* volatile instance();
 4     ...
 5 private:
 6     // one more volatile added
 7     static Singleton* volatile pInstance;
 8 };
 9 
10 // from the implementation file
11 volatile Singleton* volatile Singleton::pInstance = 0;
12 volatile Singleton* volatile Singleton::instance() {
13     if (pInstance == 0) {
14         Lock lock;
15         if (pInstance == 0) {
16             // one more volatile added
17 
分享到:
评论

相关推荐

    Android源码设计模式解析与实战.PDF(完整版)

    综上所述,Android源码设计模式解析与实战不仅是一本关于技术实现的书籍,更是一次深度学习如何优化代码结构、提高程序效率的过程。通过上述对几种典型设计模式的详细介绍,希望能帮助读者更好地理解和运用这些模式...

    设计模式精解-GoF 23种设计模式解析附C++.pdf

    - **Singleton模式**:确保一个类只有一个实例,并提供一个全局访问点。 - **Builder模式**:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 - **Prototype模式**:通过克隆已有的实例...

    Delphi 深度探索(第二版)〖含随书光盘源代码〗

    6.1.2 单例模式(singleton) 6.1.3 建造模式(builder) 6.1.4 原型模式(prototype) 6.2 结构模式 6.2.1 适配器模式(adapter) 6.2.2 合成模式(composite模式) 6.2.3 装饰模式(decorator模式) ...

    23种设计模式解析附实现源码(2nd+Edition).pdf

    《23种设计模式解析附实现源码(2nd Edition)》是一本深度解析设计模式的经典著作,书中不仅详尽地介绍了GoF所提出的23种设计模式,还提供了具体的实现代码,帮助读者更好地理解和掌握这些模式的应用。设计模式作为一...

    设计模式精解(GoF 23 种设计模式解析)

    本文档旨在通过对GoF(Gang of Four)提出的23种设计模式进行深度解析,并附带C++实现源码的方式,帮助读者深刻理解每种设计模式的核心思想及其应用场景。 #### 1. 创建型模式 **1.1 Factory模式** - **定义**:...

    二十三种设计模式【PDF版】

    设计模式之 Singleton(单态/单件) 阎宏博士讲解:单例(Singleton)模式 保证一个类只有一个实例,并提供一个访问它的全局访问点 设计模式之 Factory(工厂方法和抽象工厂) 使用工厂模式就象使用 new 一样频繁. ...

    设计模式精解(GoF 23种设计模式解析附C实现源码) pdf

    - **1.3 Singleton模式**:确保一个类只有一个实例,并提供一个全局访问点。 - **1.4 Builder模式**:将一个复杂对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。 - **1.5 Prototype模式**:用原型...

    Android源码设计模式解析与实战 pdf 版

    首先,书中涵盖了创建型设计模式,如单例模式(Singleton)、工厂模式(Factory)和建造者模式(Builder)。这些模式在Android中的应用主要体现在系统服务的管理、对象的懒加载以及复杂对象的构建过程中。例如,...

    HeadFirst设计模式源代码

    《HeadFirst设计模式源代码》是一本面向程序员的深度学习设计模式的书籍,它通过直观易懂的方式将复杂的概念转化为生动的图像和有趣的例子,帮助读者深入理解并掌握设计模式。设计模式是软件工程中的一种最佳实践,...

    Spring源码解析.pdf

    - `isSingleton(String name)`:判断指定Bean是否为单例模式。 ##### 2. DefaultListableBeanFactory - **功能**:作为`BeanFactory`的一个实现类,`DefaultListableBeanFactory`不仅提供了基本的IoC容器功能,还...

    静态的魔力:Java中静态方法和变量的深度解析

    ### 静态的魔力:Java中静态方法和变量的深度解析 #### 一、引言 Java作为一门强大的编程语言,在软件开发领域占据着举足轻重的地位。其面向对象的特性、跨平台的能力以及强大的标准库,使得Java成为开发企业级...

    面试中的设计模式的考察

    3. 单例模式(Singleton Pattern)是一种创建型设计模式,保证一个类只有一个实例,并提供一个全局访问点。这在多线程环境下保证对象的唯一性和线程安全尤其重要。 4. 代理模式(Proxy Pattern)属于结构型设计模式...

    design-pattern-java.pdf

    算法的封装与切换——策略模式(四) 模板方法模式-Template Method Pattern 模板方法模式深度解析(一) 模板方法模式深度解析(二) 模板方法模式深度解析(三) 访问者模式-Visitor Pattern 操作复杂对象结构——...

    .net core 23种设计模式

    《.NET Core 23种设计模式深度解析与实践》 设计模式,是软件开发中的经验总结,是解决常见问题的模板,对于任何开发者来说,理解并熟练运用设计模式都是提升编程技能的关键步骤。在.NET Core这个强大的跨平台开发...

    硬啃设计模式(张传波)PDF

    《硬啃设计模式》是张传波撰写的一本深度探讨设计模式的书籍,它分为五个主要部分,系统地介绍了软件开发中的设计模式及其应用。设计模式是软件工程中经过实践验证的有效解决方案,对于提高代码质量、可维护性和可...

    设计模式 - pdf 高级教程(c++语言)

    《设计模式 - 高级教程(C++语言)》是一本专为C++程序员深度解析设计模式的高级读物。设计模式是软件工程中的一种最佳实践,它总结了在特定场景下解决问题的通用方法,旨在提高代码的可复用性、可维护性和可扩展性。...

    设计模式解析1-6章

    ### 设计模式解析1-6章:面向对象与设计模式的深度剖析 #### 1. 面向对象范型的诞生与优势 面向对象编程(Object-Oriented Programming,简称OOP)作为一种重要的编程范式,旨在解决传统结构化编程面临的挑战。在...

    《设计模式》中文版

    例如,01.pdf可能介绍了创建型模式的基础概念,02.pdf和03.pdf可能分别讲解了单例模式和工厂方法模式,接着04.pdf至06.pdf可能会涉及结构型模式,而07.pdf至09.pdf则可能涵盖了行为型模式的深度解析。 通过阅读这...

    硬啃设计模式

    - **背景介绍**:作者在书中提到,自己在学习设计模式的过程中经历了“越看越晕,越晕越看”的阶段,这反映了设计模式的复杂性和深度,同时也体现了其在软件开发中的重要性。 - **学习资源**:推荐书籍包括《设计...

    java与模式 光盘内容

    《Java与模式》是一本深度探讨Java编程语言与设计模式结合的著作,旨在帮助开发者提升软件设计能力,实现更高效、可维护的代码。光盘内容包含书中所讲解的实例源代码,为读者提供了实际操作和学习的宝贵资源。在这个...

Global site tag (gtag.js) - Google Analytics