1、内存屏障指令是用来让指令变为原子指令,可以用来实现锁。
arm:
__asm__ __volatile__ ("DMB ISHST" ::: "memory");
x86:
__asm__ __volatile__ ("sfence \n" ::: "memory");
powerpc:
__asm__ __volatile__ ("lwsync" ::: "memory");
2、举例 参考程序员的自我修养--链接、装载与库(高清带完整书签版) 1.6章节
{
if (pInst == NULL)
{
lock();
if (pInst == NULL)
pInst = new T;
unlock();
}
return pInst;
}
抛开逻辑,这样的代码乍看是没有问题的,当函数返回时,PInst总是指向一个有效的对象。而lock和unlock防止了多线程竞争导致的麻烦。双重的if在这里另有妙用,可以让lock的调用开销降低到最小。读者可以自己揣摩。
但是实际上这样的代码是有问题的。问题的来源仍然是CPU的乱序执行。C++里的new其实包含了两个步骤:
(1)分配内存。
(2)调用构造函数。
所以pInst = new T包含了三个步骤:
(1)分配内存。
(2)在内存的位置上调用构造函数。
(3)将内存的地址赋值给pInst。
在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst的值已经不是NULL,但对象仍然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst==NULL为false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。那么程序这个时候会不会崩溃就取决于这个类的设计如何了。
从上面两个例子可以看到CPU的乱序执行能力让我们对多线程的安全保障的努力变得异常困难。因此要保证线程安全,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序“穿透”这个大坝。
许多体系结构的CPU都提供barrier指令,不过它们的名称各不相同,例如POWERPC提供的其中一条指令名叫lwsync。我们可以这样来保证线程安全:
#define barrier() __asm__ volatile (”lwsync”)
volatile T* pInst = 0;
T* GetInstance()
{
if (!pInst)
{
lock();
if (!pInst)
{
T* temp = new T;
barrier();
pInst = temp;
}
unlock();
}
return pInst;
}
由于barrier的存在,对象的构造一定在barrier执行之前完成,因此当pInst被赋值时,对象总是完好的。