目录

内存一致性模型(二)

第一篇文章中主要介绍了内存模型的产生原因,以及顺序一致性模型和 TSO 模型。接下来继续讲讲其它内存模型。

弱一致性模型

尽管 X86 架构放弃了之前介绍的顺序一致性模型,但是它还算是一个行为良好、至少不太离谱的架构。有许多架构,采用了一些行为更疯狂、更弱的内存模型,这些架构会产生更多的反直觉的结果。还有一种架构——SPARC 架构,允许程序员从三个不同内存模型中选择一个模型来运行程序。

采用弱一致性模型的一个典型例子是 ARM 架构。ARM 架构常用于手机、嵌入式等对实时性要求较高的设备。ARM 的内存一致性模型一直臭名昭著,因为完全没有规范化、标准化,不过从本质上来讲,它属于一种弱一致性模型,提供非常有限的一致性保证。弱一致性模型允许几乎所有操作顺序都重排,它带来的好处是允许许多不同的硬件优化,所以适用于实时性设备,坏处也很明显——编程很痛苦。

 

利用 barrier

幸运的是,所有现代 CPU 架构等包含了同步操作,利用这些机制可以在宽松模型或弱模型下很好地控制内存操作行为,以到达同步的作用。最常用的同步操作就是屏障(barrier),或者栅栏(fence)。

barrier 指令强制所有在它之前的内存操作都完成,才会继续执行它的后续的内存操作。也就是说,barrier 指令在程序执行的某一处恢复了顺序一致性。可以把 barrier 指令看成是一个 flush,只不过平时说的 flush 是针对 IO 设备的读写,此处的 flush 是针对内存。一次 barrier 操作,强制将所有 CPU core 的 L1/L2 cache,flush 到对应的内存或 L3 cache 中,使得某个 core 写入的变量,对所有 core 可见。

当然,无需同步的时候,要尽量避免使用 barrier,因为 barrier 和设计的 store buffer 以及其它优化技术,对于性能是完全相反的作用。barrier 是一个需要谨慎使用的“逃生舱口”,因为它会消耗上百个时钟周期。

另外,使用 barrier 时候,需要非常仔细,因为很容易出错,尤其在一些定义含糊的内存模型上使用。所谓定义含糊的内存模型,就是不属于某个明确的内存模型,比如自称为 TSO 模型,但却存在比 TSO 模型更宽松的操作。

也可以使用同步原语,比如原子操作 CAS,来实现同步功能。但是一般不推荐直接使用这些底层的指令。

最好的方式,还是使用一些封装好同步操作的库。

 

编译器也需要内存模型

实际中,不仅仅是硬件会重排指令顺序,程序层面,即编译器,也会在编译时通过打乱实际指令,来实现相关优化。看一个例子:

1
2
3
4
X = 0
for i in range(100):
    X = 1
    print X

这段代码打印变量 X 100次,每次都是打印 1。显然,循环中的赋值语句是多余的,因为循环过程中 X 不会改变。一个支持循环不变代码外提(loop-invariant code motion)的编译器,在翻译成机器指令时,就会把该语句放到循环外面,从而避免重复执行:

1
2
3
X = 1
for i in range(100):
    print X

这两段代码的结果是完全相同的。

现在假设有另一个线程在并行运行,它对变量 X 执行一次写入操作:

1
X = 0

这种情况下,上述两段代码的结果就完全不同了。第一段代码的输出结果可能是:

1
1111011111....

而第二段代码的结果可能是:

1
111100000000....

这意味着有了并行执行后,编译器优化后的结果和原先代码想表达的结果可能完全不同。

这个例子说明,在程序层面,同样也需要有内存模型。这里的编译器优化打乱了实际代码顺序,改变了内存访问指令在代码中的位置,而这种改变可能对程序员是可见的,也可能是不可见的。

所以,为了保留符合直觉上的代码行为,编程语言需要定义它们自己的内存模型,基于它们自己的内存模型做编译优化处理,从而在编程语言和程序员之间建立一份合约,让程序员知道使用这个语言时,内存操作是如何重排的。这个理念已经在高级语言社区内形成一种共识。典型的例子是,C++11 标准库中引入了线程库、原子操作,并且支持六种内存模型。

 

data-race-free

前面说了,内存模型相当于硬件架构或编译器和程序员之间的一个合约,而只有当存在数据竞争(data race)时,这个合约才有必要性。数据竞争是两个或多个线程对同一内存位置的访问,其中至少有一个访问是写操作,而没有用任何同步机制来对它们的访问进行排序。换句话说,如果利用某些同步机制对内存访问操作进行了同步,那么就不存在数据竞争(data-race-free),即使架构或编译器支持重排指令也没有问题,因为那些反直觉的重排指令可以在局部被同步机制禁止。

需要注意,这并不是说无数据竞争的程序,结果是可决定的(deterministic),不同的线程都可能抢先执行,所以它们执行顺序仍然无法预料。

我们可以使用一些提供同步机制的库,这些库可以帮助接管哪些复杂的重排指令的问题,使我们无需过多关注底层逻辑。


- 全文完 -

相关文章

「 您的赞赏是激励我创作和分享的最大动力! 」