转载自: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; }
尽管此代码示例大体上比较接近编译器优化代码的方式,但了解一下此代码的反汇编也很有指导意义: