改编自《程序员的自我修养》主要是为了记录和整理一下自己的思路。
一种典型的单例的实现方式是:
volatile T* pInstance = NULL;
T* getInstance(){
if(!pInstance){
lock();
if(!pInstance){
T* temp = new T();
pInstance = temp;
}
unlock();
}
return pInstance
}
说明:这里使用了个temp,而不是直接赋值给pInstance。我的理解:是因为就素直接那么写编译之后,也是相当于有个temp(寄存器)的,就是先放在寄存器,再放到pInstance对应的内存中。
1. 为什么lock放在内层?
在内层为了介绍多线程情况下上锁带来的开销。因为在单实例已经创建之后,对单实例的指针仅仅是读操作,不会产生冲突,如果读操作加锁,相当于脱裤子放屁。
2. 为什么要再次检查?
假设在pInstance还为空的时候,T1和T2同时执行lock,其中一个线程new了一个实例,然后unlock。另一个线程进入临界区,有一次new了一个实例。那就会创建多个实例。这显然会导致潜在的问题。
3. 这段代码会有什么问题?
在CPU没有动态调度的时候,这段代码是不会有问题的。
但是在CPU有动态调度的时候,
如果顺序执行,顺序是:分配内存,调用构造函数的那段代码,写指针对应的内存。
如果cpu动态调度了,顺序可能是:分配内存,写指针对应内存,调用构造函数。 (为什么能这么调度,我的理解是:写指针到对应的内存逻辑上不依赖于调用构造函数)CPU有很多实质上的寄存器供它自己玩儿动态调度,不用担心它调度不过来这么多代码。动态调度的reservation station可能会达到一百多个!)
第一个线程调用getInstance,发现指针null,进入临界区,new对象,给pInstance赋值,还没有调用构造函数,第二个线程被调度,调用getInstance,发现不为空,然后函数返回,第二个线程开始使用一个没有初始化的单实例对象。
这就是问题。
问题的根源在于:cpu调度了没有依赖的构造函数和指针赋值操作。(因为两个操作都只是读temp指针,并不对temp指针进行赋值。)
解决办法,阻止cpu调度这两端代码。
使用CPU普遍提供的barrier指令。barrier指令会组织cpu将它之前的指令被调度到它之后,也会组织它之后的指令被调度到它之前。
PowerPC上的最终代码如下:
#define barrier __asm__ volatile ("lwsync")
volatile T* pInstance = NULL;
T* getInstance(){
if(!pInstance){
lock();
if(!pInstance){
T* temp = new T();
barrier();
pInstance = temp;
}
unlock();
}
return pInstance
}
感触:
1. 并发的问题真是复杂。
2. 程序的真正执行过程可能并不像语言上形容的那样一样。比如说,C语言的顺序执行,到了动态调度就不是顺序执行了。
要真正弄清问题,还真的要想到某条语句可能对应的硬件执行流程。
3. 又印证了我的那个总结的观点:很多问题都是为了所谓的优化带来的,不管是用起来便利还是性能啊之类的。
a. 你用起来便利就可能会让别有意图的人也用起来便利。
b. 有时候性能优化的时候过于aggressive,有点单刀直入的感觉,往往会忽略某些东西~
4. CPU的动态调度,推广到其他方面,真的会把你玩死!哈哈
转载注明来自:http://blog.csdn.net/cheetach119/article/details/8990084