此代码在发布模式下挂起,但在调试模式下可以正常工作


110

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

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

25
行为上到底有什么区别?
祝朱

4
如果是Java,我将假定编译器看不到“ compile”变量的更新。在变量声明中添加“ volatile”将解决此问题(并将其设置为静态字段)。
塞巴斯蒂安

23

4
注意:这就是为什么多线程开发具有互斥和原子操作之类的原因。当您开始使用多线程时,您需要考虑一系列其他以前并不明显的内存问题。诸如互斥锁之类的线程同步工具将解决此问题。
Cort Ammon

5
@DavidSchwartz:当然可以。而且它允许编译器,运行时和CPU,以使该代码的结果比预期的不同。特别是,不允许C#使布尔访问变为非原子访问,但允许向后移动非易失性读取。相反,双原子对原子性没有这种限制。允许在两个不同步的不同线程上进行双重读取和写入操作被破坏。
埃里克·利珀特

Answers:


149

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

当然,您不能添加它,因为它是局部变量。当然,由于它是一个局部变量,因此根本就不需要它,因为局部变量保留在堆栈中,并且自然总是“新鲜”的。

但是,编译后,它不再是局部变量。由于它是在匿名委托中访问的,因此代码被拆分,并被翻译为帮助器类和成员字段,如下所示:

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;

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

调试模式的不同可能是因为在调试模式下没有进行许多优化,因此您可以调试屏幕上看到的代码。因此while (!isComplete)未进行优化,因此您可以在此处设置一个断点,因此isComplete不会在方法启动时主动将其缓存在寄存器或堆栈中,并且在每次循环迭代时都从堆上的对象中读取该值。

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

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

1)请参阅下面的Eric Lippert关于volatile和/或这篇非常有趣的文章的评论,该文章显示了确保代码所依赖volatile安全性 ..uh,良好 ..uh所涉及的复杂程度。


2
@EricLippert:哇,非常感谢您这么快地确认!您如何看待,在将来的版本中,是否有可能获得volatile捕获到关闭局部变量的选项?我想编译器可能会很难处理
。– quetzalcoatl

7
@quetzalcoatl:我不会指望很快会添加该功能。这是一种编码你想的劝阻,并没有做出更容易。此外,使事物变得易变并不一定能解决所有问题。这是一个例子,其中所有内容都是易失的,程序仍然是错误的。您可以找到错误吗?blog.coverity.com/2014/03/26/reordering-optimizations
Eric Lippert

3
明白了 我放弃了尝试去理解多线程优化的方法……这是如此的复杂。
其间的

10
@Pikoh:再次,像优化器一样思考。您有一个递增但从未读取的变量。永不读取的变量可以完全删除。
埃里克·利珀特

4
@EricLippert现在我的想法发出了点击。该线程非常有用,非常感谢。
Pikoh

82

quetzalcoatl答案是正确的。对此进行更多说明:

允许C#编译器和CLR抖动进行很多优化,并假设当前线程是唯一运行的线程。如果这些优化使程序在当前线程不是唯一正在运行的线程的情况下出现问题,那么该程序是不正确的。您需要编写多线程程序,这些程序告诉编译器并抖动您正在执行的疯狂的多线程工作。

在这种特殊情况下,允许(但不是必须)进行抖动,以观察到循环体未更改该变量,并因此得出结论:由于假设这是唯一运行的线程,因此该变量永远不会更改。如果它从不改变,则需要一次而不是每次循环都检查变量是否为真。实际上,这就是正在发生的事情。

如何解决呢? 不要编写多线程程序。即使对于专家来说,多线程也难以实现。如果必须,则使用最高级别的机制来实现您的目标。解决方案不是使变量可变。解决方案是编写一个可取消的任务,并使用任务并行库取消机制。让TPL担心正确执行线程逻辑,并正确地在线程间发送取消消息。


1
评论不作进一步讨论;此对话已转移至聊天
马达拉的幽灵

14

我附加了正在运行的进程,发现(如果我没有犯错,我对此不太习惯),该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)是首次加载且从未刷新。


8

并不是真正的答案,但可以进一步阐明该问题:

问题似乎i是在lambda主体中声明了when 并且在赋值表达式中读取了。否则,代码在发布模式下会很好地工作:

  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

我敢打赌,某些编译器或JIT优化i会弄乱事情。比我聪明的人也许可以在这个问题上阐明更多。

尽管如此,我不会对此太担心,因为我看不到相似的代码实际上可用于任何目的。


1
看到我的回答,我很确定这完全是关于'volatile'关键字的,不能将其添加到局部变量中(实际上后来又被提升为闭包中的成员字段)
。–
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.