码迷,mamicode.com
首页 > 编程语言 > 详细

C++中多线程与Singleton的那些事儿

时间:2015-02-01 00:25:38      阅读:374      评论:0      收藏:0      [点我收藏+]

标签:

前言

  前段时间在网上看到了一个百度的面试题,大概意思是如何在不使用锁和C++11的情况下,用C++实现线程安全的Singleton。

  看到这个题目后,第一个想法就是用Scott Meyer在《Effective C++》中提到的,把non-local static变量放到static成员函数中来实现,但是经过一番查找轮子,这种实现在某些情况下是有问题的。本文主要将从最基本的单线程中的Singleton开始,慢慢讲述多线程与Singleton的那些事。

单线程

  在多线程下,下面这个是常见的写法:

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         if (!value_)
 8         {
 9             value_ = new T();
10         }
11         return *value_;
12     }
13 
14 private:
15     Singleton();
16     ~Singleton();
17 
18     static T* value_;
19 };
20 
21 template<typename T>
22 T* Singleton<T>::value_ = NULL;

在单线程中,这样的写法是可以正确使用的,但是在多线程中就不行了。

多线程加锁

  在多线程的环境中,上面单线程的写法就会产生race condition从而产生多次初始化的情况。要想在多线程下工作,最容易想到的就是用锁来包含shared variable了。下面是伪代码:

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         {
 8             MutexGuard guard(mutex_)  // RAII
 9             if (!value_)
10             {
11                 value_ = new T();
12             }
13         }
14         return *value_;
15     }
16 
17 private:
18     Singleton();
19     ~Singleton();
20 
21     static T*     value_;
22     static Mutex  mutex_;
23 };
24 
25 template<typename T>
26 T* Singleton<T>::value_ = NULL;
27 
28 template<typename T>
29 Mutex Singleton<T>::mutex_;

这样在多线程下就能正常工作了。这时候,可能有人会站出来说这种做法每次调用getInstance的时候都会进入临界区,在频繁调用getInstance的时候会比较影响性能。这个时候,DCL写法出现了。

DCL

  DCL即double-checked locking。在普通加锁的写法中,每次调用getInstance都会进入临界区,这样在heavy contention的情况下该函数就会成为系统性能的瓶颈,这个时候就有先驱者们想到了DCL写法,也就是进行两次check,当第一次check为假时,才加锁进行第二次check:

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         if(!value_)
 8         {
 9             MutexGuard guard(mutex_);
10             if (!value_)
11             {
12                 value_ = new T();
13             }
14         }
15         return *value_;
16     }
17 
18 private:
19     Singleton();
20     ~Singleton();
21 
22     static T*     value_;
23     static Mutex  mutex_;
24 };
25 
26 template<typename T>
27 T* Singleton<T>::value_ = NULL;
28 
29 template<typename T>
30 Mutex Singleton<T>::mutex_;

是不是觉得这样就完美啦?其实在一段时间内,大家都以为这种做法正确的、有效的做法。幸运的是,后来有大牛们发现了DCL中的问题,避免了这样错误的写法在更多的程序中出现。

  那么到底错在哪呢?我们先看看第12行value_ = new T这一句发生了什么:

  1. 分配了一个T类型对象所需要的内存。
  2. 在分配的内存出构造T类型的对象。
  3. 把分配的内存的地址赋给指针value_

  主观上,我们会觉得计算机在会按照123的步骤来执行代码,但是问题就出在这。实际上只能确定步骤1最先执行,而步骤2、3的执行顺序却是不一定的。假如某一个线程A在调用getInstance的时候第12行的语句按照132的步骤执行,那么当刚刚执行完步骤3的时候发生线程切换,计算机开始执行另外一个线程B。因为第一次check没有锁保护,那么在线程B中调用getInstance的时候,不会在第一此check上等待,而是执行这一句,那么此时value_已经被赋值了,就会直接返回该值然后执行后面使用T对象的语句,但是在A线程中步骤3还没有执行!也就是说在B线程中通过getInstance返回的对象还没有被构造就被拿去使用了!这样就会发生一些难以debug的灾难。

  volatile关键字也不会影响执行顺序的不确定性。

  在多核心机器的环境下,2个核心同时执行上面的A、B两个线程时,由于第一次check没有锁保护,依然会出现使用实际没有被构造的对象这些情况。

  关于DCL问题的详细介绍,可以参考Scott Meyer的paper:《C++ and the Perils of Double-Checked Locking》

  不过在新的C++11中,这个问题得到了解决。因为新的C++11规定了新的内存模型,保证了上述的执行顺序是123,DCL又可以正确使用了,不过在C++11下却有更简洁的多线程Singleton写法了,这个留在后面再介绍。

  关于新的C++11的内存模型,可以参考:C++11中文版FAQ:内存模型C++11FAQ:Memory ModelC++ Data-Dependency Ordering: Atomics and Memory Model

Meyers Singleton

  Scott Meyer在《Effective C++》中提出了一种简洁的singleton写法

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         static T value;
 8         return value;
 9     }
10 
11 private:
12     Singleton();
13     ~Singleton();
14 };

  先说结论:

  • 单线程下,正确。
  • C++11及以后的版本(如C++14)的多线程下,正确。
  • C++11之前的多线程下,不一定正确。

  原因在于在C++11之前的标准中并没有规定local static变量的内存模型,所以很多编译器在实现local static变量的时候仅仅是进行了一次check(参考《深入探索C++对象模型》),于是getInstance函数被编译器改写成这样了:

 1 bool initialized = false;
 2 char value[sizeof(T)];
 3 
 4 T& getInstance()
 5 {
 6     if (!initialized)
 7     {
 8        initialized = true;
 9        new (value) T();
10     }
11     return *(reinterpret_cast<T*>(value));
12 }

于是乎它就是不是线程安全的了。

  但是在C++11却是线程安全的,这是新的C++标准规定了当一个线程正在初始化一个变量的时候,其他线程必须等到该初始化完成以后才能访问它。

  在C++11 standard中的§6.7 [stmt.dcl] p4:

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

  在stackoverflow中的Is Meyers implementation of Singleton pattern thread safe?这个问题中也有讨论到。

  不过有些编译器在C++11之前的版本就支持这种模型,例如g++,从g++4.0开始,meyers singleton就是线程安全的,不需要C++11。其他的编译器需要具体的去查相关的官方手册了。

Atomic Singleton

  在C++11之前的版本下,除了通过锁实现线程安全的Singleton外,还可以利用各个编译器内置的atomic operation来实现。(假设类Atomic是封装的编译器提供的atomic operation)

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         while (true)
 8         {
 9             if (ready_.get())
10             {
11                 return *value_;
12             }
13             else
14             {
15                 if (initializing_.getAndSet(true))
16                 {
17                     // another thread is initializing, waiting in circulation
18                 }
19                 else
20                 {
21                     value_ = new T();
22                     ready_.set(true);
23                     return *value_;
24                 }
25             }
26         }
27     }
28 
29 private:
30     Singleton();
31     ~Singleton();
32 
33     static Atomic<bool>  ready_;
34     static Atomic<bool>  initializing_;
35     static T*            value_;
36 };
37 
38 template<typename T>
39 Atomic<int> Singleton<T>::ready_(false);
40 
41 template<typename T>
42 Atomic<int> Singleton<T>::initializing_(false);
43 
44 template<typename T>
45 T* Singleton<T>::value_ = NULL;

  肯定还有其他的写法,但是思路都是要区分三种状态:

  • 对象已经构造完成
  • 对象还没有构造完成,但是某一线程正在构造中
  • 对象还没有构造完成,也没有任何线程正在构造中

pthread_once

  如果是在unix平台的话,除了使用atomic operation外,在不适用C++11的情况下,还可以通过pthread_once来实现Singleton。

  pthread_once的原型为

int pthread_once(pthread_once_t *once_control, void (*init_routine)(void))

  APUE中对于pthread_once是这样说的:

如果每个线程都调用pthread_once,系统就能保证初始化话例程init_routine只被调用一次,即在系统首次调用pthread_once时。

  所以,我就可以这样来实现Singleton了

 1 template<typename T>
 2 class Singleton : Nocopyable
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         threads::pthread_once(&once_control_, init);
 8         return *value_;
 9     }
10 
11 private:
12     static void init()
13     {
14         value_ = new T();
15     }
16 
17     Singleton();
18     ~Singleton();
19 
20     static pthread_once_t  once_control_;
21     static T*              value_;
22 };
23 
24 template<typename T>
25 pthread_once_t Singleton<T>::once_control_ = PTHREAD_ONCE_INIT;
26 
27 template<typename T>
28 T* Singleton<T>::value_ = NULL;

  如果我们需要正确的释放资源的话,可以在init函数里面通过glibc提供的atexit函数来注册释放函数,从而达到了只在进程退出时才释放资源的这一目的。

static object

  现在再回头看看本文开头说的面试题的要求,不用锁和C++11,那么可以通过atomic operation来实现,但是有人会说atomic不是夸平台的,各个编译器的实现不一样。那么其实通过static object来实现也是可行的。

 1 template<typename T>
 2 class Singleton
 3 {
 4 public:
 5     static T& getInstance()
 6     {
 7         return *value_;
 8     }
 9 
10 private:
11     Singleton();
12     ~Singleton();
13 
14     class Helper
15     {
16     public:
17         Helper()
18         {
19             Singleton<T>::value_ = new T();
20         }
21 
22         ~Helper()
23         {
24             delete value_;
25             value_ = NULL;
26         }
27     };
28 
29     friend class Helper;
30 
31     static T*      value_;
32     static Helper  helper_;
33 };
34 
35 template<typename T>
36 T* Singleton<T>::value_ = NULL;
37 
38 template<typename T>
39 typename Singleton<T>::Helper Singleton<T>::helper_;

  这种写法有一个前提就是不能在main函数执行之前调用getInstance,因为C++标准只保证静态变量在main函数之前之前被构造完成。

local static

  上面一种写法只能在进入main函数后才能调用getInstance,那么有人说,我要在main函数之前调用怎么办?

  嗯,办法还是有的。这个时候我们就可以利用local static来实现,C++标准包装函数内的local static变量在函数调用之前被初始化构造完成,利用这一特性我们可以这样来做

 1 template<typename T>
 2 class Singleton
 3 {
 4 private:
 5     Singleton();
 6     ~Singleton();
 7 
 8     class Creater
 9     {
10     public:
11         Creater()
12             : value_(new T())
13         {
14         }
15 
16         ~Creater()
17         {
18             delete value_;
19             value_ = NULL;
20         }
21 
22         T& getValue()
23         {
24             return *value_;
25         }
26 
27         T* value_;
28     };
29 
30 public:
31     static T& getInstance()
32     {
33         static Creater creater;
34         return creater.getValue();
35     }
36 
37 private:
38     class Dummy
39     {
40     public:
41         Dummy()
42         {
43             Singleton<T>::getInstance();
44         }
45     };
46 
47     static Dummy dummy_;
48 };
49 
50 template<typename T>
51 typename Singleton<T>::Dummy Singleton<T>::dummy_;

  这样就可以了。dummy_作用是即使在main函数之前没有调用getInstance,它依然会作为最后一道屏障保证在进入main函数之前构造完成Singleton对象。

参考资料

[1] 梅耶 (Scott Meyers). Effective C++. 电子工业出版社, 2011

[2] 斯坦利·B.李普曼. 深入探索C++对象模型. 电子工业出版社, 2012

[3] 陈良桥(译). C++11 FAQ中文版

[4] Bjarne Stroustrup. C++11 FAQ

[5] C++11 standard

[6] 史蒂文斯 (W.Richard Stevens). UNIX环境高级编程, 人民邮电出版社, 2014

[7] stackoverflow. Is Meyers implementation of Singleton pattern thread safe?

(完)

C++中多线程与Singleton的那些事儿

标签:

原文地址:http://www.cnblogs.com/liyuan989/p/4264889.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!