单例的Double check问题和CPU动态调度共存时的线程安全问题。


改编自《程序员的自我修养》主要是为了记录和整理一下自己的思路。

一种典型的单例的实现方式是:


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

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值