【问题标题】:This code hangs in release mode but works fine in debug mode此代码在发布模式下挂起,但在调试模式下工作正常
【发布时间】:2017-01-13 10:27:24
【问题描述】:

我遇到了这个问题,想知道在调试和发布模式下出现这种行为的原因。

public static void Main(string[] args)
{            
   bool isComplete = false;

   var t = new Thread(() =>
   {
       int i = 0;

        while (!isComplete) i += 0;
   });

   t.Start();

   Thread.Sleep(500);
   isComplete = true;
   t.Join();
   Console.WriteLine("complete!");
}

【问题讨论】:

  • 行为到底有什么不同?
  • 如果是 java 我会假设编译器看不到“编译”变量的更新。在变量声明中添加 'volatile' 可以解决这个问题(并使其成为静态字段)。
  • 啊,Heisenbug
  • 注意:这就是为什么多线程开发具有互斥锁和原子操作之类的东西。当您开始使用多线程时,您需要考虑一组以前并不明显的额外内存问题。像互斥锁这样的线程同步工具可以解决这个问题。
  • @DavidSchwartz:这当然是允许的。并且编译器、运行时和 CPU允许使该代码的结果与您期望的不同。特别是,C# 不允许进行 bool 访问 nonatomic,但允许将非易失性读取及时向后移动。相比之下,Double 对原子性没有这种限制。在没有同步的情况下在两个不同的线程上进行双重读写是允许被撕裂的。

标签: c#


【解决方案1】:

我猜优化器被isComplete 变量上缺少'volatile' 关键字愚弄了。

当然,你不能添加它,因为它是一个局部变量。当然,因为它是一个局部变量,所以根本不需要它,因为局部变量保存在 stack 上,它们自然总是“新鲜”的。

不过,编译后,不再是局部变量。由于它是在匿名委托中访问的,因此代码被拆分,并被转换为辅助类和成员字段,例如:

public static void Main(string[] args)
{
    TheHelper hlp = new TheHelper();

    var t = new Thread(hlp.Body);

    t.Start();

    Thread.Sleep(500);
    hlp.isComplete = true;
    t.Join();
    Console.WriteLine("complete!");
}

private class TheHelper
{
    public bool isComplete = false;

    public void Body()
    {
        int i = 0;

        while (!isComplete) i += 0;
    }
}

我现在可以想象,多线程环境中的 JIT 编译器/优化器在处理 TheHelper 类时,实际上可以在开始时将值 false 缓存在某个寄存器或堆栈帧中Body() 方法,在方法结束之前永远不要刷新它。那是因为没有保证线程和方法在“=true”执行之前不会结束,所以如果没有保证,那么为什么不缓存它并获得一次读取堆对象而不是每次读取它的性能提升迭代。

这正是关键字 volatile 存在的原因。

为了让这个帮助类在多线程环境中正确 稍微好一点 1),它应该有:

    public volatile bool isComplete = false;

但是,当然,因为它是自动生成的代码,所以您不能添加它。更好的方法是在对isCompleted 的读写周围添加一些lock()s,或者使用其他一些现成的同步或线程/任务实用程序,而不是尝试使用裸机(它 不会是裸机,因为它是 CLR 上的 C#,带有 GC、JIT 和 (..))。

调试模式的差异可能是因为在调试模式中排除了许多优化,所以你可以调试你在屏幕上看到的代码。因此while (!isComplete) 未优化,因此您可以在那里设置断点,因此isComplete 不会在方法开始时积极缓存在寄存器或堆栈中,而是在每次循环迭代时从堆上的对象中读取。

顺便说一句。这只是我对此的猜测。我什至没有尝试编译它。

顺便说一句。这似乎不是一个错误。它更像是一个非常模糊的副作用。另外,如果我是对的,那可能是语言缺陷 - C# 应该允许将“volatile”关键字放在被捕获并提升为闭包中的成员字段的局部变量上。

1) 见下文 Eric Lippert 关于 volatile 的 cmets 和/或这篇非常有趣的文章,展示了确保依赖于 volatile 的代码是 所涉及的复杂程度安全 ..uh,good ..uh,让我们说OK。

【讨论】:

  • @EricLippert:哇,非常感谢你这么快就确认了!您如何看待,在未来的某个版本中,我们是否有机会在捕获到关闭局部变量上获得 volatile 选项?我想编译器可能有点难以处理..
  • @quetzalcoatl:我不会指望很快就会添加该功能。这是您想要不鼓励,而不是变得更容易的那种编码。此外,让东西变得易变并不一定能解决所有问题。这是一个例子,一切都是不稳定的,程序仍然是错误的;你能找到错误吗? blog.coverity.com/2014/03/26/reordering-optimizations
  • 明白。我放弃尝试理解多线程优化......它有多复杂。
  • @Pikoh:再一次,像优化器一样思考。您有一个递增但从不读取的变量。一个永远不会被读取的变量可以被完全删除。
  • @EricLippert 现在我的想法是点击一下。这个帖子提供了非常丰富的信息,非常感谢,真的。
【解决方案2】:

answer of quetzalcoatl 是正确的。为了更清楚地了解它:

允许 C# 编译器和 CLR 抖动进行大量优化,假设当前线程是唯一运行的线程。如果这些优化使程序在当前线程不是唯一运行的线程的世界中不正确那是你的问题。你需要编写多线程程序,告诉编译器和抖动你在做什么疯狂的多线程事情。

在这种特殊情况下,抖动是允许的——但不是必需的——观察循环体没有改变变量,因此得出结论——因为假设这是唯一运行的线程——变量将从不改变。如果它从不改变,则需要检查变量的真值一次,而不是每次都通过循环。事实上,这就是正在发生的事情。

如何解决这个问题? 不要编写多线程程序。即使对于专家来说,多线程也很难做到正确。如果必须,请使用最高级别的机制来实现您的目标。这里的解决方案不是使变量易失。这里的解决方案是编写一个可取消的任务,并使用任务并行库取消机制。让 TPL 担心如何正确处理线程逻辑以及跨线程正确发送取消。

【讨论】:

【解决方案3】:

我附上运行的进程发现(如果我没有犯错,我对这个不是很熟练)Thread方法翻译成这样:

debug051:02DE04EB loc_2DE04EB:                            
debug051:02DE04EB test    eax, eax
debug051:02DE04ED jz      short loc_2DE04EB
debug051:02DE04EF pop     ebp
debug051:02DE04F0 retn

eax(其中包含isComplete 的值)是第一次加载并且从不刷新。

【讨论】:

    【解决方案4】:

    不是一个真正的答案,而是对这个问题有更多的了解:

    问题似乎是当i 在 lambda 主体内声明时 它是 only 在赋值表达式中读取的。否则,代码在发布模式下运行良好:

    1. i 在 lambda 主体之外声明:

      int i = 0; // Declared outside the lambda body
      
      var t = new Thread(() =>
      {
          while (!isComplete) { i += 0; }
      }); // Completes in release mode
      
    2. i 未在赋值表达式中读取:

      var t = new Thread(() =>
      {
          int i = 0;
          while (!isComplete) { i = 0; }
      }); // Completes in release mode
      
    3. i 也在其他地方被读取:

      var t = new Thread(() =>
      {
          int i = 0;
          while (!isComplete) { Console.WriteLine(i); i += 0; }
      }); // Completes in release mode
      

    我敢打赌,关于 i 的一些编译器或 JIT 优化会搞砸事情。比我聪明的人可能会更清楚地了解这个问题。

    不过,我不会太担心它,因为我看不出类似的代码实际上会在什么地方发挥作用。

    【讨论】:

    • 看我的回答,我很确定这都是关于 'volatile' 关键字的,它不能被添加到局部变量中(实际上后来被提升为闭包中的成员字段)..
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-09
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多