[zz from newsmth]王大牛的Memory Model白话系列(1)

发信人: yifanw (王轶凡), 信区: CPlusPlus
标  题: 内存模型之白话入门
发信站: 水木社区 (Sun Mar 15 12:49:50 2009), 站内

程序员眼中的多处理器共享的内存:

    作为一个程序员,很容易把多处理器的内存想象成这样:每个处理器的 load & store
会按照program order执行;内存在同一时刻只能接受一个处理器的load/store。

    毫无疑问,程序员们非常喜欢这个模型,它与单处理器下的内存模型是一致的,这是我
们最习惯的编程方式,而且为单处理器写出的多线程程序可以很安全的拿到多处理器上运行。

    但是,这个模型的性能很差,阻止了很多的CPU和编译器的优化,为了获得高效率,几                                         
乎所有编译器和CPU都在不同程度上破坏了这个模型。

CPU和编译器对这个模型的破坏:

    现代的CPU基本上都支持out-of-order execution、multiple instruction issue和
memory reorder buffer(write buffer),这就使得load & store的program order无法保
证。当然啦,这种reorder是在不影响程序正确性的前提下进行的,但是CPU只能保证当前                                          
thread的正确性,没有顾及对其他CPU上的thread的影响。

    此外,在NUMA的机器(现在高端的x86也有NUMA的机器)上,一个CPU对不同的memory
module的访问延迟是不同的。所以即使两个store按照program order被执行,它们完成(被
其他的CPU看到)的顺序也会被改变。

    很多的编译优化也会打乱load/store的program order(甚至把变量装在register中而
移除相应的load & store )。这些优化包括:code motion、register allocation、
common sub-expression elimination、 software pipelining等等。与CPU的
out-of-order一样,编译器的out-of-order只会保证当前thread的正确性,没有顾及对其他
thread的影响。

一些具体的会出错例子:

初始状态:flag1=flag2=0
Thread 1 on processor 1:
      flag1=1;
      if (flag2==0) then
           enter critical section

Thread 2 on processor 2:
      flag2=1;
      if (flag1==0) then
            enter critical section

由于编译器或是CPU可能会在thread 1上先load flag2再store flag1;在thread 2上先
load flag1再store flag2,从而使两个thread同时进入critical section!

初始状态:B=0
Thread1 on processor 1:
       A=1000;
       B=1;

Thread2 on processor 2:
       while(B!=1);
       C=A;

由于CPU或是编译器可能会reorder thread1中对A、B的store,C的值不一定是1000。更可能
的情况是编译器会把B的值装在register中,而使thread2进入一个死循环!

    下面我们看一个更加复杂,并且更加现实的例子:
Singleton* Singleton::instance () {
if (pInstance == 0) {
    Lock lock;
    If (pInstance == 0) {
        pInstance = new Singleton();
    }
}
return pInstance;
}
    这是一段用double checked locking来实现singleton的经典代码,使用它的程序不计
其数,但是它有bug:它不是thread-safe的!甚至在单处理器上,也不能保证正确工作。
    按照program order,CPU应该先调用Singleton的constructor(一般来说,里面会包含
一些store),然后再赋值给pInstance(一个store)。但是编译器可能会inline
Singleton(),然后reorder这些store,使得Singleton的constructor还没有执行完,
pInstance的值已经不再等于0,造成的结果就是其他的thread会得到一个不完整的
Singleton! 即使编译器不做任何的reorder,CPU也有能做同样的事情!
--
欢迎来CSARCH版


※ 来源:·水木社区 newsmth.net·[FROM: 222.67.1.*]                         
现阶段的方法是:

pInstance = Singleton();
改成:
Singleton* tmp = Singleton();
//在这里用汇编插入一个write barrier,阻止write-write的reorder
pInstance = tmp;

在VC2005中,可以把pInstance的声明改成:
Singleton * volatile pInstane;
vc2005保证:
在一个volatile variable的write操作之前的,所有的load/write都不会reorder到他后面,compiler在必要的时候会插入CPU的barrier

在Java/C#中,volatile都有类似的保证,可以把pInstance声明称volatile,来解决这个问题。

C++ 0x中,没有使用volatile,而是搞了一个atomic library,要把pInstance做成atomic variable。对他的store会有类似的保证
                                                                    

posted @ 2009-03-16 16:01  破冰  阅读(481)  评论(0编辑  收藏  举报