易失性怪异现象

10

我觉得大多数人都知道下面这个问题,在 Release 模式下构建时会发生(代码来自于 C# 中的线程):

static void Main()
{
  bool complete = false; 

  var t = new Thread (() =>
  {
    bool toggle = false;
    while (!complete) toggle = !toggle;
  });

  t.Start();
  Thread.Sleep (1000);

  complete = true;
  t.Join();        // Blocks indefinitely
}

由于编译器优化会缓存complete的值,从而阻止子线程看到更新的值。

然而,稍微改变上述代码:

class Wrapper
{
    public bool Complete { get; set; }
}

class Test
{
    Wrapper wrapper = new Wrapper();

    static void Main()
    {
        var test = new Test();
        var t = new Thread(() =>
        {
            bool toggle = false;
            while (!test.wrapper.Complete) toggle = !toggle;
        });

        t.Start();
        Thread.Sleep(1000);

        test.wrapper.Complete = true;
        t.Join();        // Blocks indefinitely
    }
}

不使用volatile、内存屏障或任何引入隐式屏障的机制,即可解决问题(即子线程能够在1秒后退出)。

添加完成标志的封装如何影响线程之间的可见性?


1
你的代码不能保证一定能够运行,但也不一定会失败。你不应该依赖编译器的优化来保证正确性(或者在这种情况下,错误性)。 - svick
1
@svick:我其实不是。这只是我无意中注意到的一件事情。 - Tudor
1
可能是A reproducible example of volatile usage的重复问题。 - Hans Passant
2个回答

6
我认为你已经在问题中找到了答案:
由于编译器优化缓存了完整值,从而阻止子线程看到更新后的值。
编译器/JIT优化会在合适/被视为安全和合理的情况下执行。因此,当优化未按预期发生时,可能有很好的原因(某人检测到了这种用法模式并阻止了优化),或者只是没有被优化(最有可能)。

5
关键点在于这是未定义行为。编译器团队可能决定明天以与第一个示例相同的方式优化属性,也可能不优化第一个示例(或以不同的方式这样做),使其无法停止。 - Servy
我同意你的观点,很可能是这种情况。我只是试图找出一个模式,以便在确定像这样的代码是否会失败时能够发现它。 - Tudor

0

第一种情况是本地变量的简单别名传播。编译器会非常积极地进行这样的操作(http://en.m.wikipedia.org/wiki/Aliasing_(computing))。第二种情况,即使由于属性与成员变量的语法相似而看起来相同,但涉及到方法调用(getter/setter),因此不能简单地进行简化。


在这种情况下,它不是简单地设置值。运行时不能像那样“忽略”某些东西。 - Aniket Inge
我不明白你所说的“不是简单的值设置”。别名传播是一种编译器技术,用于简化或替换变量。更多信息请参考:http://en.wikipedia.org/wiki/Aliasing_(computing) - nakhli

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接