线程安全与过度优化

线程安全与过度优化

样例1

x=0 
thread1    thread2
lock();       lock();
x++;           x--;
unlock();    unlock();

即使加锁,也不一定能保证x的结果为2,这是由于编译器为了优化,提高x的访问速度,把x放入放到某个编译器中,不同线程的寄存器是各自独立的,假如thread1先获得锁,如下

(1)thread1获得锁;
(2)thread1读取x的值放入寄存器R1中,R1=0;
(3)thread1中R1++,R1=1,由于之后可能会继续访问x,因此thread1暂不(4)将R1写回x;
(5)thread2读取x的值放入寄存器R2中,R2=0;
(6)thread2中R2++,R2=1;
(7)thread2将R2写回到x(x=1);
(8)thread1很久之后将R1写回到x(x=1);

样例2

x=y=0;
thread1  thread2
x=1;        y=1;
r1=y;       r2=x;

逻辑上r1与r2必有一个为1,不可能同时为0.然而事实上,r1,r2同时为0的情况确实可能会发生,原因是cpu在执行程序的时候为了提高效率有可能交换指令的顺序。同样,编译器在进行优化的时候可能交换毫不相干的两条相邻指令,如(x=1和r1=y)的执行顺序,交换后如下

x=y=0;
thread1  thread2
r1=y;      y=1;
x=1;      r2=x;

那么r1=r2=0就有可能了,可以使用volatile阻止编译器将值读取到寄存器中,每次都读取到内存中,但是不能防止编译器进行指令交换。

样例3

volatile T* pInst=0;
T* GetInstance()
{
	if(pInst==nullptr)
	{
		lock()
		if(pInst==nullptr)
		{
			pInst=new T;
		}
		unlock();
	}
	return pInst;
}

这段代码表面上看去没有问题,但是实际上代码是有问题的,c++中的new操作,包含两个步骤,
1、从堆中空闲分区链表上查找合适分区,分配内存。
2、调用构造函数

所以pInst=new T包含三个步骤

1、分配内存
2、在分配的内存上调用构造函数
3、将内存首地址赋值给pInst

由于cpu交换指令操作,在这三个步骤中,2和3的顺序可以颠倒的,也就是说程序可能会将还没有调用构造函数的地址赋值给pInst,那么此时这个程序会不会报错就不得而知了。

从上面两个例子可以看著,要保证线程安全必须得阻止cpu换序,barrier指令可以阻止cpu将该指令之前的指令换序到该指令之后。换句话说,就是barrier指令的作用类似一个拦水坝,防穿透。
可以如下:

volatile T* pInst=0;
T* GetInstance()
{
	if(pInst==nullptr)
	{
		lock()
		if(pInst==nullptr)
		{
			T* temp=new T;
			barrier();
			pInst=temp;
		}
		unlock();
	}
	return pInst;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值