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

Fel*_*oto 47 .net c# multithreading thread-safety memory-barriers

在"C#4 in a Nutshell"中,作者表明这个类有时可以写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);
}
Run Code Online (Sandbox Code Playgroud)

这种需要对我来说似乎很疯狂.如何识别出现这种情况的所有可能情况?我认为如果处理器改变了操作顺序,它需要保证行为不会改变.

你还懒得使用障碍吗?

Bri*_*eon 66

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

但是,ECMA规范的内存模型较弱.因此,从理论上讲,Mono甚至.NET Framework的未来版本可能会开始展示错误的行为.

所以我所说的是,删除障碍#1和#2不太可能对程序的行为产生任何影响.当然,这不是保证,而是基于CLR当前实施的观察.

消除障碍#3和#4肯定会产生影响.这实际上很容易重现.好吧,本身不是这个例子,但下面的代码是一个比较着名的演示.它必须使用Release版本进行编译,并在调试器之外运行.错误是程序没有结束.您可以通过调用循环Thread.MemoryBarrier内部while或标记stop为来修复错误volatile.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

一些线程错误难以重现的原因是因为用于模拟线程交错的相同策略实际上可以修复错误.Thread.Sleep是最值得注意的例子,因为它会产生内存障碍.您可以通过在while循环内放置一个调用并观察该bug消失来验证这一点.

您可以在此处查看我的答案,以便对您引用的书中的示例进行另一次分析.

  • @Brian Gideon可能是非常愚蠢的,但我不明白为什么你的例子中的程序永远不会结束!为什么`while(!stop)`永远不会得到`false`?你能解释一下吗?或者你可以建议一些详细的博客吗?我读过OP贴的文章(albahari的例子),我也在那里感到困惑; 对我来说,障碍1就足够了,为什么还有其他障碍呢?MSDN说,"MemoryBarrier"可以防止指令重新排序.那么为什么只有屏障1是不够的(因为`完成`不能在设置'answer`之前执行)?我真的不明白. (4认同)
  • @mshsayem 有几个原因。1) 在线程函数中,就编译器而言,`while(!stop)` 将始终为真,编译器将看到线程函数不会操作该变量并忽略该检查(除非你做`lock`或`Interlocked.Read`或类似的)。编译器不保证发出的 IL 不会优化它,除非你明确地这样做。 (2认同)
  • @mshsayem 2) 一些较旧的 CPU(例如 IA64,AKA“Itanium”)不保证线程之间的缓存一致性(在硬件级别上),例如,如果上面的代码碰巧在两个不同的内核上运行,则没有保证运行另一个线程的 CPU 会费心询问另一个 CPU 将 `stop` 设置为什么,并假设它的缓存值(始终为 false)是正确的。这就是引入 `lock` ASM 前缀的目的,但据我所知,这与具有更强内存保证的 AMD64 CPU 无关。 (2认同)

Han*_*ant 10

第二个任务甚至开始运行时,第一个任务完成的几率非常好.如果两个线程同时运行该代码并且没有中间缓存同步操作,则只能观察到此行为.在您的代码中有一个,StartNew()方法将在某个地方的线程池管理器中进行锁定.

让两个线程同时运行此代码非常困难.此代码在几纳秒内完成.您将不得不尝试数十亿次并引入可变延迟以获得任何赔率.当然没有太多指向,真正的问题是,当你期望它时会随机发生这种情况.

远离这个,使用lock语句编写健全的多线程代码.