此代码挂起在发布模式,但在debugging模式下工作正常

我碰到这个,想知道在debugging和发布模式下这种行为的原因。

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!"); } 

我猜想优化器被isCompletevariables缺less'volatile'关键字所迷惑。

当然,你不能添加它,因为它是一个局部variables。 当然,因为它是一个局部variables,所以根本就不需要,因为当地人是堆积如山,自然总是“新鲜”的。

编译之后,它不再是局部variables 。 由于它是在一个匿名委托中进行访问的,因此代码被拆分,并被转换为一个辅助类和成员字段,如下所示:

 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; } } 

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

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

对于这个辅助类是正确 的,稍微好一点 1)在multithreading环境中,它应该有:

  public volatile bool isComplete = false; 

但是,当然,因为它是自动生成的代码,所以不能添加它。 一个更好的方法是在读取和写入isCompleted添加一些lock() ,或者使用一些其他随时可用的同步或线程/任务实用程序,而不是尝试使用裸机(它不会裸机,因为它是C#CLR与GC,JIT和(..))。

debugging模式的差异可能是因为在debugging模式下排除了许多优化,所以您可以debugging您在屏幕上看到的代码。 因此while (!isComplete)没有被优化,所以你可以在那里设置一个断点,因此isComplete在方法启动时不会被积极地caching在寄存器或堆栈中,并且在每次循环迭代时从堆中的对象读取。

BTW。 这只是我的猜测。 我甚至没有尝试编译它。

BTW。 这似乎不是一个错误; 这更像是一个非常模糊的副作用。 另外,如果我说得对,那么这可能是一种语言缺陷–C#应该允许将“volatile”关键字放在捕获并提升到闭包成员字段的局部variables上。

1)请参阅下面的Eric Lippert关于volatile和/或这个非常有趣的文章的评论,其中显示了确保依赖于volatile代码是安全的 ,这个复杂程度涉及到很多..uh, 好的 ..uh,让我们说OK。

quetzalcoatl的答案是正确的。 为了阐明它:

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

在这种特殊情况下,允许抖动(但不是必需的),观察到variables不被循环体所改变,从而得出结论 – 因为通过假设,这是唯一的线程运行 – variables永远不会改变。 如果它永远不会改变,那么这个variables需要被检查一次 ,而不是每一次循环。 事实上,这正在发生。

如何解决这个问题? 不要编写multithreading程序 。 即使对于专家来说,multithreading也很难保持正确。 如果你必须,那么使用最高级别的机制来实现你的目标 。 这里的解决scheme不是使variables易变。 这里的解决scheme是写一个可取消的任务,并使用任务并行库取消机制 。 让TPL担心正确地获取线程逻辑,并正确地发送消除跨线程。

我附加到正在运行的进程,发现(如果我没有犯错误,我不是很熟悉这个) 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的值)被第一次加载,永远不会刷新。

这并不是一个真正的答案,而是在这个问题上进一步阐明:

这个问题似乎是当i在lambda体内声明只能在赋值expression式中读取。 否则,代码在发布模式下运行良好:

  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没有阅读任务expression式:

     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 

我的赌注是一些编译器或JIT优化关于i搞砸的东西。 比我更聪明的人可能会对这个问题有更多的了解。

尽pipe如此,我不会太担心,因为我没有看到类似的代码实际上可以用于任何目的。