线程安全 与编译器和CPU

这里就不具体谈什么是线程安全了,简单说,就是对于多个线程对数据并发访问不会产生错误。

线程安全与编译器和CPU之间有些“见不得人”的勾当在里面,如果你想仅依靠加锁,释放锁来达到对于数据的安全访问,那么就忽视了下面2个问题了。


1 编译器

这里直接上一段代码:(x的初值为0)

thread1:

lock();

x++;

unlock();


thread2:

lock();

x++;

unlock();

这里x的值一定会是2吗?如果说编译器没有对x进行优化,那么在程序正常运行的情况下x的值的确会是2。

考虑到x后面还会被访问,因此编译器可能将它放入寄存器,下次访问的时候直接从寄存器中读取值,那么x的值就有可能为1了,比如执行顺序如下所示:

1  线程一将x的值存入寄存器R[1](=0)

2  R[1]++(考虑到后续还要对其进行访问,线程一暂时不把它写入内存)

3  线程二将x的值存入寄存器R[2](=0)

4  R[2]++

5  线程二将R[2]写入内存

6  线程一将R[1]写入内存

此时x的值就变成了1.

因此,我们在考虑线程安全的时候,还需要注意编译器因优化而将变量放入寄存器的问题。


在说明解决办法之前让我们再看一下第二个情况。

x=y=0;

thread1:

x=1

r1=y;


thread2:

y=1;

r2=x;


是不是觉得r1和r2中必定至少会有一个的值为1,然而真实情况是r1=r2=0的情况是有可能发生的。

原因有两点    1     CPU为了提升效率而对指令动态的进行了调度。

2     编译器在进行优化的时候,对指令进行了交换。比如说上面的代码执行顺序可能为如下:

thread1:

r1=y;

x=1;

thread2:

y=1;

r2=x;

 这样的话,显然r1,r2可能会同时为0


那么如何解决上面的问题呢?

我们可以通过volatile关键字来避免编译器所做的“错事”。

volatile关键字可以做到如下两点:

1    阻止编译器为了提高访问速度而将变量缓存到寄存器而不写回

2    阻止编译器调整volatile变量的指令顺序



2 CPU

可是通过volatile就万事大吉了?当然不是,上面也说了CPU也可能为了提升效率而对指令进行调整。这里还有一个十分经典的例子:

volatile T* pInst=NULL;

T* getInstance(){

ifpInst==NULL){

lock();

if(pInst==NULL)

pInst=new T;

unlock();

}

return pInst;

}

(PS: 大家可以想想这里双重if进行判断有什么好处)

我们先来分析

pInst=new T;

这行代码会进行什么操作

1 申请一块内存

2 在这块内存处调用T的构造函数

3 将内存地址赋值给pInst


咋看上去没啥问题,但是由于CPU的动态调度,上述操作的执行顺序很有可能会变成

1->3->2

即先将内存地址赋值给了pInst再对其初始化,假设现在有另一个线程调用getInstance函数呢? 此时,pInst已经不为NULL了,那么该线程对于T对象的后续操作就很有可能出现错误了。

既然如此,如果我们要保证线程安全的话,在这里需要阻止CPU换序。

通常情况下我们可以调用CPU提供的一条指令,我们常称其为:barrier;(目前还没有具有可移植的阻止换序的方法)

barrier指令会阻止CPU将该指令前的指令交换到该指令之后,同理它也会阻止它之后的指令交换到它之前。

许多体系的CPU都提供barrier指令,不多大多名称都不相同。

比如在POWERPC中,提供的指令是lwsync;

因此我们可以采用如下代码来保证线程安全:

#define barrier()   __asm__  volatile ("lwsync")

volatile T* pInst=NULL;

T* getInstance(){

ifpInst==NULL){

lock();

if(pInst==NULL)

{

T* temp=new T;

barrier();

pInst=temp;

}

unlock();

}

return pInst;

}


如此,我们便可以保证真正的线程安全了。

内容参考自《程序员的自我修养》




你可能感兴趣的:(linux,cpu,线程安全,编译器)