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

前言

  前段时间在网上看到了一个百度的面试题,大概意思是如何在不使用锁和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?

(完)

时间: 2024-10-10 19:27:49

C++中多线程与Singleton的那些事儿的相关文章

【设计模式】C++中多线程与Singleton的那些事儿

本文转自:http://liyuanlife.com/blog/2015/01/31/thread-safe-singleton-in-cxx/ 1. 前言 前段时间在网上看到了个的面试题,大概意思是如何在不使用锁和C++11的情况下,用C++实现线程安全的Singleton. 看到这个题目后,第一个想法就是用Scott Meyer在<Effective C++>中提到的,在static成员函数中构造local static变量的方法来实现,但是经过一番查找.思考,才明白这种实现在某些情况下是

Python有了asyncio和aiohttp在爬虫这类型IO任务中多线程/多进程还有存在的必要吗?

最近正在学习Python中的异步编程,看了一些博客后做了一些小测验:对比asyncio+aiohttp的爬虫和asyncio+aiohttp+concurrent.futures(线程池/进程池)在效率中的差异,注释:在爬虫中我几乎没有使用任何计算性任务,为了探测异步的性能,全部都只是做了网络IO请求,就是说aiohttp把网页get完就程序就done了. 结果发现前者的效率比后者还要高.我询问了另外一位博主,(提供代码的博主没回我信息),他说使用concurrent.futures的话因为我全

Android中多线程编程(四)AsyncTask类的详细解释(附源码)

Android中多线程编程中AsyncTask类的详细解释 1.Android单线程模型 2.耗时操作放在非主线程中执行 Android主线程和子线程之间的通信封装类:AsyncTask类 1.子线程中更新UI 2.封装.简化异步操作. 3.AsyncTask机制:底层是通过线程池来工作的,当一个线程没有执行完毕,后边的线程是无法执行的.必须等前边的线程执行完毕后,后边的线程才能执行. AsyncTask类使用注意事项: 1.在UI线程中创建AsyncTask的实例 2.必须在UI线程中调用As

Delphi中多线程用消息实现VCL数据同步显示

Delphi中多线程用消息实现VCL数据同步显示 Lanno Ckeeke 2006-5-12 概述: delphi中严格区分主线程和子主线程,主线程负责GUI的更新,子线程负责数据运算,当数据运行完毕后,子线程可以向主线程式发送消息,以便通知其将VCL中的数据更新. 实现: 关键在于消息的发送及接收.在消息结构Tmessage中wParam和lParam类型为Longint,而指针类型也定义为Longint,可以通过此指针来传递自己所感兴趣的数据.如传递字符数组: 数组定义: const MA

iOS中多线程基本概念

进程与线程 什么是进程? 近程是指在系统中正在运行的一个应用程序. 每个近程之间是独立的,每个近程均运行在其专用且受保护的内存空间内. **可以通过“活动监视器”可以查看Mac系统中所有开启的进程. 什么是线程? 一个进程要想执行任务,必须得有线程(每一个进程至少要有一条线程). 线程是进程的基本执行单元,一个进程(程序)的所有任务都在线程中执行. 线程的串行 一个线程中任务的执行是串行(顺序执行的) 如果要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务. 也就是说,在同一时间内

java中多线程模拟(多生产,多消费,Lock实现同步锁,替代synchronized同步代码块)

import java.util.concurrent.locks.*; class DuckMsg{ int size;//烤鸭的大小 String id;//烤鸭的厂家和标号 DuckMsg(){ } DuckMsg(int size, String id){ this.size=size; this.id=id; } public String toString(){ return id + " 大小为:" + size; } } class Duck{ private int

iOS中多线程的实现方案

我去, 好蛋疼, 刚刚写好的博客就因为手贱在触控板上右划了一下, 写的全丢了, 还得重新写, 博客园就没有针对这种情况的解决方案吗?都不想写了 一. iOS中多线程的实现方案有四种 (1) NSThread陷阱非常多, 有缺陷, 不过是OC的, 偶尔用一下 (2) GCD在苹果在iOS4推出的, 能充分利用设备的多核, 而且不用考虑线程, 性能比NSThread好的多 GCD研究起来就比较深了, 所以在面试的时候会经常被问到 (3) NSOperation封装了很多使用的使用的功能, 某些情况下

spring中bean的singleton属性

1.  UserDao.java package com.lxh.springSingleton; public interface UserDao { // 保存User public void save(); } 2.  UserDaoImpl.java package com.lxh.springSingleton; public class UserDaoImpl implements UserDao { @Override public void save() { System.out

IIS各个版本中你需要知道的那些事儿

一.写在前面 目前市面上所用的IIS版本估计都是>=6.0的.所以我们主要以下面三个版本进行讲解 服务器版本 IIS默认版本 server2003 6.0 server2008 7.0 server2012 8.0 二.IIS6的请求过程 由图可知,所有的请求会被服务器中的http.sys组件监听到,它会根据IIS中的 Metabase 查看基于该 Request 的 Application 属于哪个Application Pool, 如果该Application Pool不存在,则创建之.否则