为什么我们需要Thread.MemoryBarrier()?

在“C#4简介”中,作者表示这个类有时可以写0,没有MemoryBarrier ,虽然我不能在我的Core2Duo中重现:

 public class Foo { int _answer; bool _complete; public void A() { _answer = 123; //Thread.MemoryBarrier(); // Barrier 1 _complete = true; //Thread.MemoryBarrier(); // Barrier 2 } public void B() { //Thread.MemoryBarrier(); // Barrier 3 if (_complete) { //Thread.MemoryBarrier(); // Barrier 4 Console.WriteLine(_answer); } } } private static void ThreadInverteOrdemComandos() { Foo obj = new Foo(); Task.Factory.StartNew(obj.A); Task.Factory.StartNew(obj.B); Thread.Sleep(10); } 

这个需求对我来说似乎很疯狂。 我怎么能认识到所有可能发生的情况? 我认为,如果处理器改变操作顺序,则需要保证行为不会改变。

你打扰使用障碍吗?

你将很难再现这个bug。 事实上,我甚至会说你将永远无法使用.NET Framework来重现它。 原因是因为微软的实现使用强大的内存模型进行写入。 这意味着写入被视为不稳定。 易失性写入具有locking释放语义,这意味着所有先前写入必须在当前写入之前提交。

但是,ECMA规范有一个较弱的内存模型。 所以理论上讲,Mono甚至.NET Framework的未来版本都可能开始展示bug的行为。

所以我说的是,排除#1和#2的障碍对于程序的行为是不会有什么影响的。 当然,这不是一个保证,而只是基于目前CLR的实施而进行的观察。

去除屏障#3和#4肯定会产生影响。 这实际上很容易重现。 那么,本身不是这个例子,但下面的代码是更为人所知的演示之一。 必须使用Release版本进行编译,并在debugging器之外运行。 错误在于程序没有结束。 您可以通过调用Thread.MemoryBarrierwhile循环内或通过将stop标记为volatile来修复此bug。

 class Program { static bool stop = false; public static void Main(string[] args) { var t = new Thread(() => { Console.WriteLine("thread begin"); bool toggle = false; while (!stop) { toggle = !toggle; } Console.WriteLine("thread end"); }); t.Start(); Thread.Sleep(1000); stop = true; Console.WriteLine("stop = true"); Console.WriteLine("waiting..."); t.Join(); } } 

一些线程错误难以重现的原因是因为用于模拟线程交错的相同策略实际上可以修复错误。 Thread.Sleep是最显着的例子,因为它会产生内存障碍。 您可以通过在while循环中放置一个调用来validation该bug是否消失。

你可以在这里看到我的答案,从你引用的书的例子进行另一个分析。

第二项任务甚至开始运行时,第一项任务完成的可能性非常好。 如果两个线程同时运行该代码,并且没有干预caching同步操作,则只能观察到这种行为。 在你的代码中有一个,StartNew()方法会在某个地方的线程池pipe理器中获取一个锁。

获得两个线程同时运行这个代码是非常困难的。 此代码在几纳秒内完成。 你将不得不尝试数十亿次,并引入可变延迟有任何可能性。 当然没有太多的指出,真正的问题是当你期望的时候,这是随机发生的。

远离这一点,使用locking语句来编写理智的multithreading代码。

如果你使用volatilelock ,内存屏障是内置的。但是,是的,你确实需要它。 话虽如此,我怀疑你需要一半的例子显示。

它很难再现multithreading的错误 – 通常你必须多次运行testing代码(数千次),并有一些自动检查,如果发生错误,将标记。 您可能会尝试在一些行之间添加一个简短的Thread.Sleep(10),但是这并不总是保证您会得到与没有它相同的问题。

内存障碍是为那些需要对自己的multithreading代码进行真正的核心性能优化的人所引入的。 在大多数情况下,使用其他同步原语(如volatile或lock)会更好。

我只会引用multithreading的优秀文章之一:

考虑下面的例子:

 class Foo { int _answer; bool _complete; void A() { _answer = 123; _complete = true; } void B() { if (_complete) Console.WriteLine (_answer); } } 

如果方法A和B在不同的线程上同时运行,B可能写“0”吗? 答案是肯定的,原因如下:

编译器,CLR或CPU可能会重新排列程序指令以提高效率。 编译器,CLR或CPU可能会引入caching优化,以便对其他线程立即看不到variables赋值。 C#和运行时非常小心,以确保这样的优化不会破坏普通的单线程代码 – 或正确使用锁的multithreading代码。 在这些场景之外,您必须通过创build内存屏障(也称为内存屏蔽)来明确地打败这些优化,以限制指令重新sorting和读/写caching的影响。

完整的栅栏

最简单的内存屏障是一个完整的内存屏障(full fence),它可以防止围绕该围栏的任何types的指令重新sorting或caching。 调用Thread.MemoryBarrier生成完整的栅栏; 我们可以通过应用四个完整的栅栏来修复我们的例子:

 class Foo { int _answer; bool _complete; void A() { _answer = 123; Thread.MemoryBarrier(); // Barrier 1 _complete = true; Thread.MemoryBarrier(); // Barrier 2 } void B() { Thread.MemoryBarrier(); // Barrier 3 if (_complete) { Thread.MemoryBarrier(); // Barrier 4 Console.WriteLine (_answer); } } } 

Thread.MemoryBarrier背后的所有理论,以及为什么我们需要在非阻塞场景中使用它来使代码安全和健壮的描述很好: http : //www.albahari.com/threading/part4.aspx

如果您曾经触摸过两个不同线程的数据,可能会发生这种情况。 这是处理器用于提高速度的技巧之一 – 您可以构build不这样做的处理器,但是速度会更慢,所以没有人再这样做。 你可能应该读一些像Hennessey和Patterson的东西来认识所有不同types的竞赛条件。

我总是使用某种更高级别的工具,比如监视器或者锁,但是在内部他们正在做类似的事情或者实施了障碍。