理论与实践中的 C# 内存模型,第 2 部分

时间:2021-10-18 00:41:46

转载自:https://msdn.microsoft.com/zh-cn/magazine/jj883956.aspx

这是介绍 C# 内存模型的系列文章的第二篇(共两篇)。 正如在 MSDN 杂志十二月刊的第一篇文章 (msdn.microsoft.com/magazine/jj863136) 中所介绍的,编译器和硬件可能会悄然改变程序的内存操作,尽管其方式不会影响单线程行为,但可能会影响多线程行为。 例如,请考虑以下方法:

 

void Init() {   _data = 42;   _initialized = true; }

如果 _data 和 _initialized 是普通(即,非可变)字段,则允许编译器和处理器对这些操作重新排序,以便 Init 执行起来就像是用以下代码编写的:

 

void Init() {   _initialized = true;   _data = 42; }

在上一篇文章中,我介绍了抽象 C# 内存模型。 本文将介绍如何在 Microsoft .NET Framework 4.5 支持的不同体系结构上实际实现 C# 内存模型。

编译器优化

正如在第一篇文章中提到的,编译器可能通过对内存操作进行重新排序来优化代码。 在 .NET Framework 4.5 中,,将 C# 编译为 IL 的 csc.exe 编译器并不执行大量的优化操作,因此该编译器不会对内存操作进行重新排序。 但将 IL 编译为机器码的实时 (JIT) 编译器实际上将执行一些对内存操作进行重新排序的优化,我将在下文对此予以介绍。

循环读取提升 请考虑下面的轮询循环模式:

 

class Test {   private bool _flag = true;   public void Run()   {     // Set _flag to false on another thread     new Thread(() => { _flag = false; }).Start();     // Poll the _flag field until it is set to false     while (_flag) ;     // The loop might never terminate! } }

在这个示例中,.NET 4.5 JIT 编译器可能按如下所示重写循环:

 

if (_flag) { while (true); }

对于单线程而言,此项转换完全合法,并且将读取提升出循环通常是一种出色的优化方法。 但如果在另一个线程上将 _flag 设置为 false,则优化可能导致挂起。

请注意,如果 _flag 字段是可变字段,则 JIT 编译器不会将读取提升出循环。 (有关对此模式更详细的介绍,请参见我在十二月发表的文章中的“轮询循环”部分。)

读取消除 以下示例说明了另一个可能导致多线程代码出现错误的编译器优化:

 

class Test {   private int _A, _B;   public void Foo()   {     int a = _A;     int b = _B;     ... } }

此类包含两个非可变字段:_A 和 _B。 方法 Foo 先读取字段 _A,然后读取字段 _B。 但由于这两个字段是非可变字段,因此编译器可*地对两个读取进行重新排序。 因此,如果算法的正确与否取决于读取顺序,则程序将包含错误。

很难想象编译器通过交换读取顺序将获得什么结果。 根据 Foo 的编写方式,编译器可能不会交换读取顺序。

但如果我在 Foo 方法的顶部再添加一个无关紧要的语句,则确实会进行重新排序:

 

public bool Foo() {   if (_B == -1) throw new Exception(); // Extra read   int a = _A;   int b = _B;   return a > b; }

在 Foo 方法的第一行上,编译器将 _B 的值加载到寄存器中。 然后,_B 的第二次加载将仅使用寄存器中已有的值,而不发出实际的加载指令。

实际上,编译器将按如下所示重写 Foo 方法:

 

public bool Foo() {   int b = _B;   if (b == -1) throw new Exception(); // Extra read   int a = _A;   return a > b; }

尽管此代码示例大体上比较接近编译器优化代码的方式,但了解一下此代码的反汇编也很有指导意义: