CPU动态调换执行指令
以前,我从某些书籍上有看到编译器在优化代码的时候会改变C++代码的执行顺序;其实CPU为了优化执行的效率也可能会动态改变代码执行顺序。
以下内容来自《程序员的自我修养--链接、装载与库》
一段典型的double-check的singleton代码如下:
volatile T* pInst = 0; T* GetInstance() { if (pInst == NULL) { lock(); if (pInst == NULL) pInst = new T(); unlock(); } return pInst; }
抛开逻辑,这样的代码是没有问题的,当函数返回时,pInst总是指向一个有效对象。而lock和unlock防止多线程竞争导致的麻烦。
但是实际上这样的代码是有问题的。问题的来源任然是CPU的乱序执行。C++里的new其实包含了两个步骤:
(1)分配内存。
(2)调用构造函数。
所以pInst = new T()包含了三个步骤:
(1)分配内存。
(2)在内存位置上调用构造函数。
(3)将内存地址赋值给pInst。
在这三步中,(2)和(3)的顺序是可以颠倒的。也就是说,完全有可能出现这样的情况:pInst的值已经不是NULL,但对象任然没有构造完毕。这时候如果出现另外一个对GetInstance的并发调用,此时第一个if内的表达式pInst==NULL为false,所以这个调用会直接返回尚未构造完全的对象的地址(pInst)以提供给用户使用。
为此,阻止CPU换序是必需的。遗憾的是,现在并不存在可移植的阻止换序的方法。通常情况下是调用CPU提供的一条指令,这条指令常常被称为barrier。一条barrier指令会阻止CPU将该指令之前的指令交换到barrier之后,反之亦然。换句话说,barrier指令的作用类似于一个拦水坝,阻止换序“穿透”这个大坝。
许多体系结构的CPU都提供barrier指令,不过它们的名称各不相同,例如POWERPC提供的其中一条指令名叫lwsync。我们可以这样来保证线程安全:
#define barrier() __asm__ volaticle("lwsync") volatile T* pInst = 0; T* GetInstance() { if (pInst == NULL) { lock(); if (pInst == NULL) { T* tmp = new T(); barrier(); pInst = tmp; } unlock(); } return pInst; }
由于barrier的存在,对象的构造一定在barrier执行之前完成,因此当pInst被赋值时,对象总是完好的。