从C++单例模式到线程安全
2016-07-14 23:00
281 查看
先看一个最简单的教科书式单例模式:
有2个要点:
1.private的构造函数和=操作符,用于防止类外的实例化和被复制;
2.static的类指针和get方法。
在大多数单线程情况下,以上代码大都会运行得很好,除非遇到中断:
1.当程序运行到tag1 处触发了中断;
2.中断处理程序恰调用的也是getInstance函数。
可想而知,这和多线程的情况类似,假设线程A 运行到tag1处,还没来得及new,此时ps仍然是NULL,线程B(或中断处理程序) 同时也运行到此通过if判断,那么将会实例化2个CSingleton对象,显然是不对的。
为了解决上述问题,自然而然,最容易想到也最常用的方法是加锁,因此getInstance改成这样:
加了锁以后貌似解决了上述问题,但也同样带来了新的问题:如果程序到处是诸如:
这样的调用,除了第一次的lock()有用外,后面的都是在做无用功,lock()的代价说大不大,但在某些情况下还是会提高程序延迟,这对追求完美的程序猿来说是完全无法接受的。
于是乎,咱想出了一个办法:
DCLP很好地解决了多次调用不必要的lock()。
然而,你们以为这样就完了?too young。。
DCLP在多线程下仍然存在2个根本问题:
1.程序的指令执行顺序不确定;
2.编译器优化问题。
先说2,在某些编译器下,以上的两个if判断只会执行一个,甚至一个都不执行,原因是编译器认为至少有一个if判断是多余的,它自动帮助我们优化了代码。
再说1,ps = new CSingleton; 这条语句会被拆分为这样的三个步骤执行:
1.为要new的对象开辟一块内存;
2.构造该对象,填入这块内存;
3.将ps指针指向这块内存。
以上三个步骤,2和3的顺序是不确定的,可能先2后3,也可能先3后2。。。
实际执行时可能是这样的:
如果编译器按上述顺序执行代码,考虑如下状况:
线程A 执行到step 1还未执行后面的step 2,此时ps非空,但其指向的内存里面的内容还未被构造出来,于此同时线程B 进入这个函数,判断ps非空直接返回ps,但是调用者此时访问的ps内存实际内容CSingleton还没被构造呢,这是一块地址正确大小正确但内部数据不明的东西,当然会出错(调用者一般这么调用:CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();........此时的aa,bb,cc是啥玩意儿?)。
这也是为什么加上volatile关键字仍然不可以解决同步问题,volatile只解决了编译器优化问题,却无法控制机器指令执行顺序。
很遗憾的是,C/C++本身在设计时是不考虑多线程问题的,也就是说,要处理多线程问题还要程序猿自己想办法填坑。。
说了这么多,我们要讨论的问题仍然没有解决,庆幸的是,C++ 11提供了内存栅栏技术来解决这个问题,这里不赘述,有兴趣的读者可以自己搜索资料看看,不过是一些api调罢了。
那么,C++ 11 以前的代码如何解决这个问题呢?很不幸,并没有很好的解决方案,一种可行的方案是,程序中不要到处这么调用这个单例对象:
而是在程序开始就初始化缓存这个单例对象:
参考资料:C++ and the Perils of Double-Checked Locking ∗Scott Meyers and Andrei AlexandrescuSeptember 2004
http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
class CSingleton { public: static CSingleton* getInstance() { if (NULL == ps) {//tag1 ps = new CSingleton; } return ps; } private: CSingleton(){} CSingleton & operator=(const CSingleton &s); static CSingleton* ps; }; CSingleton* CSingleton::ps = NULL;
有2个要点:
1.private的构造函数和=操作符,用于防止类外的实例化和被复制;
2.static的类指针和get方法。
在大多数单线程情况下,以上代码大都会运行得很好,除非遇到中断:
1.当程序运行到tag1 处触发了中断;
2.中断处理程序恰调用的也是getInstance函数。
可想而知,这和多线程的情况类似,假设线程A 运行到tag1处,还没来得及new,此时ps仍然是NULL,线程B(或中断处理程序) 同时也运行到此通过if判断,那么将会实例化2个CSingleton对象,显然是不对的。
为了解决上述问题,自然而然,最容易想到也最常用的方法是加锁,因此getInstance改成这样:
static CSingleton* getInstance() { lock();//伪代码 if (NULL == ps) { ps = new CSingleton; } return ps; }
加了锁以后貌似解决了上述问题,但也同样带来了新的问题:如果程序到处是诸如:
CSingleton::getInstance()->aaaa(); CSingleton::getInstance()->bbbb(); CSingleton::getInstance()->cccc();
这样的调用,除了第一次的lock()有用外,后面的都是在做无用功,lock()的代价说大不大,但在某些情况下还是会提高程序延迟,这对追求完美的程序猿来说是完全无法接受的。
于是乎,咱想出了一个办法:
static CSingleton* getInstance() { if (NULL == ps)//这里加了次判断,只有第一次才会为true而调用lock() { lock();//伪代码 if (NULL == ps) { ps = new CSingleton; } } return ps; }很久以后我才知道,这个方法有个很高大上的名字,叫做双重检查锁定模式,简称DCLP(Double Checked Locking Pattern)。
DCLP很好地解决了多次调用不必要的lock()。
然而,你们以为这样就完了?too young。。
DCLP在多线程下仍然存在2个根本问题:
1.程序的指令执行顺序不确定;
2.编译器优化问题。
先说2,在某些编译器下,以上的两个if判断只会执行一个,甚至一个都不执行,原因是编译器认为至少有一个if判断是多余的,它自动帮助我们优化了代码。
再说1,ps = new CSingleton; 这条语句会被拆分为这样的三个步骤执行:
1.为要new的对象开辟一块内存;
2.构造该对象,填入这块内存;
3.将ps指针指向这块内存。
以上三个步骤,2和3的顺序是不确定的,可能先2后3,也可能先3后2。。。
实际执行时可能是这样的:
static CSingleton* getInstance() { if (NULL == ps) { lock();//伪代码 if (NULL == ps) { //伪代码 ps = xx;//step 3 new sizeof(CSingleton);//step 1 new CSingleton;//step 2 } } return ps; }
如果编译器按上述顺序执行代码,考虑如下状况:
线程A 执行到step 1还未执行后面的step 2,此时ps非空,但其指向的内存里面的内容还未被构造出来,于此同时线程B 进入这个函数,判断ps非空直接返回ps,但是调用者此时访问的ps内存实际内容CSingleton还没被构造呢,这是一块地址正确大小正确但内部数据不明的东西,当然会出错(调用者一般这么调用:CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();........此时的aa,bb,cc是啥玩意儿?)。
这也是为什么加上volatile关键字仍然不可以解决同步问题,volatile只解决了编译器优化问题,却无法控制机器指令执行顺序。
很遗憾的是,C/C++本身在设计时是不考虑多线程问题的,也就是说,要处理多线程问题还要程序猿自己想办法填坑。。
说了这么多,我们要讨论的问题仍然没有解决,庆幸的是,C++ 11提供了内存栅栏技术来解决这个问题,这里不赘述,有兴趣的读者可以自己搜索资料看看,不过是一些api调罢了。
那么,C++ 11 以前的代码如何解决这个问题呢?很不幸,并没有很好的解决方案,一种可行的方案是,程序中不要到处这么调用这个单例对象:
CSingleton::getInstance()->aa(); CSingleton::getInstance()->bb(); CSingleton::getInstance()->cc();
而是在程序开始就初始化缓存这个单例对象:
CSingleton* const g_ps = CSingleton::getInstance();//程序一开始就缓存这个单例对象 g_ps->aa(); g_ps->bb(); g_ps->cc();但是如此带来的问题是程序一开始就实例化了这个单例对象,对象在整个程序的声明周期存在,这貌似叫饿汉式,而之前那种叫懒汉式,孰轻孰重,只有根据实际情况取舍了。
参考资料:C++ and the Perils of Double-Checked Locking ∗Scott Meyers and Andrei AlexandrescuSeptember 2004
http://www.aristeia.com/Papers/DDJ_Jul_Aug_2004_revised.pdf
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Python3写爬虫(四)多线程实现数据爬取
- 关于指针的一些事情
- 设计模式之创建型模式 - 特别的变量问题
- c++ primer 第五版 笔记前言
- share_ptr的几个注意点
- C#实现多线程的同步方法实例分析
- Lua中调用C++函数示例
- 浅谈chuck-lua中的多线程
- Lua教程(一):在C++中嵌入Lua脚本
- Lua教程(二):C++和Lua相互传递数据示例
- C#简单多线程同步和优先权用法实例
- C#多线程学习之(四)使用线程池进行多线程的自动管理
- C#多线程编程中的锁系统(三)
- 解析C#多线程编程中异步多线程的实现及线程池的使用
- C#多线程学习之(六)互斥对象用法实例
- 基于一个应用程序多线程误用的分析详解
- C#单例模式(Singleton Pattern)实例教程
- C++联合体转换成C#结构的实现方法
- C#多线程学习之(三)生产者和消费者用法分析