内存模型之白话解决方案

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的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值