线程安全与过度优化
样例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;
}