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

发信人: yifanw (王轶凡), 信区: CPlusPlus
标  题: 内存模型之白话解决方案
发信站: 水木社区 (Sun Mar 15 17:02:31 2009), 站内

99%的情况下:

    大多数程序员在写多线程程序的时候,都是使用os或是thread library提供的同步原语
(semaphore、lock/spinlock、monitor等)把共享变量包围起来。这些同步原语都保证对
共享变量的读写会在临界区内完成,不会乱序到lock之前,也不会乱序到unlock之后。所以
99%的程序员是不需要的关心memory ordering的,只要全部且正确的使用了各种锁,基本上
就可以写出线程安全的程序。

    上一篇白话入门中,double checked locking就是属于不想在quick-path上使用锁,做
了一个不成熟的优化,而引入了bug。

1%的情况下:

    由于种种原因,你不想使用lock来做同步,这就需要了解memory ordering,并采用如
下解决方案:

(A) 山寨解决方案:

    (1)阻止编译器的优化
    针对编译器的优化,可以使用volatile来修饰相应的share variable,编译器*应该*不
会reorder那些volatile variable的read & write,也不会把它们装到register中。但是,
volatile的语义是各个compiler自己决定的,所以在使用之前,最好详细了解。
Visual C++ 2005中volatile的功能很强,文挡也很清楚:
http://msdn2.microsoft.com/en-us/library/12a04hfd.aspx;
我没有能找到gcc关于volatile的详细文档。

    (2)阻止CPU的优化
     Intel提供了一些指令,来解决CPU的memory ordering的问题:
lfence:一个针对load的fence,在lfence之前所有的load(按照program order)都会在
lfence之前执行;在lfence之后的所有load(按照program order)都会在lfence之后执行


sfence:一个针对store的fence,在sfence之前所有的store(按照program order)都会在
sfence之前执行;在sfence之后所有的store(按照program order)都会在sfence之后执行

mfence:等同于lfence + sfence,在mfence之前所有的load & store(按照program
order)都会在mfence之前执行;在mfence之后所有的load & store(按照program order)
都会在mfence之后执行。

此外,带lock prefix的instruction都相当于执行了一个mfence。

(B) 完全解决方案:

    山寨解决方案不是很完美,需要程序员了解CPU、汇编和编译器优化,而且程序移植到
其他系统上也会有一些麻烦。各个语言社区都在想办法把这个底层细节抽象出来,使得程序
员用一个很简单优雅的方式就能指定需要memory ordering,把实现细节留给编译器。

    于是,有了两个术语:
    (1) acquire语义: 如果操作A拥有acquire语义,A后面的所有的load, store都不会乱
序到A之前
    (2) release语义: 如果操作C拥有release语义,C前面的所有的load, store都不会乱
序到C之后


就是说acquire操作是一个向后的barrier,没有东西可以跑到它前面去;release操作是一
个向前的barrier,没有东西可以到它后面去。

|======> acquire       release <=======|

在Java/C#/VC2005中,对一个volatile variable的read和对各种lock的加锁拥有acquire语
义;对一个volatile variable的write和对各种lock的解锁拥有release语义。

在C++ 0x中,对volatile没有相应的规定,但是对atomic variable(使用atomic library)
的读写和对锁的加解锁也都有类似的acquire/release语义。

这里也解释了为什么使用锁之类的东西,基本不会受到memory ordering的困扰:共享变量
的write/read是不会乱序到临界区之外的,而我们只在临界区内读写共享变量。

只要我们使用acquire/release来指明程序需要的memory ordering,就可以不关心编译器的
优化、CPU的乱序(编译器会做掉剩下的时候,比如压制优化,生成指令来阻止CPU乱序)。

下面来看一个非常简单的C#的例子:

using System;
using System.Threading;
class Test {
    public static int result;
    public static volatile bool finished;

    static void Thread2() {
        result = 143;
        finished = true;
    }

    static void Main() {
        finished = false;

        new Thread(new ThreadStart(Thread2)).Start();

        for (;;) {
            if (finished) {
                Console.WriteLine("result = {0}", result);
                return;
            }
        }
    }
}

  finished被用来作为thread2退出的标志,需要被两个thread共享,所以我们把它做成
volatile,来防止编译器优化(把它装在register中)和CPU乱序(防止Thread2()中
finished先于result被赋值,或者Main中result先于finished被读取)。

但是result同样是一个共享变量,为什么不需要做成volatile呢?因为没有这个必要,首先
考虑Thread2()中对finished的赋值,这是一个volatile write,拥有release语义,这可以
保证对result的赋值必然先完成;Main中对finished的读取是一个volatile read,拥有
acquire语义,这可以保证对result的读取必然后完成。所以,result是不需要做成
volatile的。


--
欢迎来CSARCH版


※ 修改:·yifanw 于 Mar 15 17:29:55 2009 修改本文·[FROM:222.67.1.*]                                                   
※ 来源:·水木社区 newsmth.net·[FROM:222.67.1.*]           
                                                           

你可能感兴趣的:(memory)