当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《此代码在发布模式下挂起,但在调试模式下工作正常》经验,为你挑选了4个好方法。

我遇到过这个,想知道调试和发布模式下这种行为的原因.

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

quetzalcoatl.. 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()s读取和写入isCompleted,或使用一些其他即用型同步或线程/任务实用程序,而不是尝试裸金属(它不会是裸机,因为它是带有GC,JIT和(..)的CLR上的C#.

调试模式的差异可能是因为在调试模式下排除了许多优化,因此您可以调试在屏幕上看到的代码.因此while (!isComplete)没有进行优化,因此您可以在那里设置断点,因此isComplete不会在方法启动时在寄存器或堆栈中主动缓存,并在每次循环迭代时从堆上的对象读取.

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

BTW.它似乎不是一个bug; 它更像是一种非常模糊的副作用.此外,如果我是对的,那么它可能是语言不足 - C#应该允许将"volatile"关键字放在被捕获并提升到闭包中的成员字段的局部变量上.

1)请参阅下面的Eric Lippert关于volatile和/或这篇非常有趣的文章的评论,该文章显示了确保依赖的代码volatile安全的复杂程度..呃, ......呃,让我们说好的.



1> quetzalcoatl..:

我猜这个优化器被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()s读取和写入isCompleted,或使用一些其他即用型同步或线程/任务实用程序,而不是尝试裸金属(它不会是裸机,因为它是带有GC,JIT和(..)的CLR上的C#.

调试模式的差异可能是因为在调试模式下排除了许多优化,因此您可以调试在屏幕上看到的代码.因此while (!isComplete)没有进行优化,因此您可以在那里设置断点,因此isComplete不会在方法启动时在寄存器或堆栈中主动缓存,并在每次循环迭代时从堆上的对象读取.

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

BTW.它似乎不是一个bug; 它更像是一种非常模糊的副作用.此外,如果我是对的,那么它可能是语言不足 - C#应该允许将"volatile"关键字放在被捕获并提升到闭包中的成员字段的局部变量上.

1)请参阅下面的Eric Lippert关于volatile和/或这篇非常有趣的文章的评论,该文章显示了确保依赖的代码volatile安全的复杂程度..呃, ......呃,让我们说好的.


@Pikoh:再次,像优化者一样思考.你有一个增量但从未读过的变量.永远不会读取的变量可以完全删除.
@quetzalcoatl:我不会指望很快就会添加这个功能.这是你想要*劝阻*的那种编码,而不是*变得更容易*.此外,使东西变得不稳定并不一定能解决所有问题.这是一个例子,其中一切都是易变的,程序仍然是错误的; 你能找到这个bug吗?http://blog.coverity.com/2014/03/26/reordering-optimizations/
@EricLippert现在我的想法点击了一下.这个帖子非常有用,非常感谢,真的.
了解.我放弃尝试理解多线程优化......它是多么复杂的疯狂.
@EricLippert:哇,非常感谢你确认这么快!您如何看待,在未来的某个版本中我们是否有机会在捕获到闭包的局部变量上获得"volatile"选项?我想编译器处理起来有点困难.
现在,如果你想知道优化器为什么做出一些选择而不是其他选择,那么*尝试编写一个优化器*,即使只是在脑海中.如果你必须写一个优化器,你会如何优化`i + = 0;`?可能完全消除它.所以现在循环是空的,这意味着循环只是一个测试和一个goto.循环中没有任何东西可以改变测试*因为循环中根本没有任何东西*因此问题是"可以观察测试是否会导致或取决于副作用?".现在假设循环中有*any*其他代码; 这如何改变你的分析?
@Mehrdad:从芯片设计人员到操作系统设计人员,再到语言设计人员,再到用户,都有很多责任.所有人都对性能,易用性和可靠性抱有期望; 不幸的是,这些事情经常发生冲突.我个人是否希望内存模型更强大,即使以牺牲性能为代价?是.我是否希望多线程工具箱中的首选工具具有比锁更好的属性?绝对.但是,我们通过妥协于许多相互冲突的目标来达到这种状况.

2> Eric Lippert..:

quetzalcoatl的答案是正确的.为了更多地了解它:

允许C#编译器和CLR抖动进行大量优化,假设当前线程是唯一运行的线程.如果这些优化使程序在当前线程不是唯一运行的问题的线程中不正确.您需要编写多线程程序,告诉编译器和抖动您正在做的疯狂多线程的东西.

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

怎么解决这个? 不要编写多线程程序.即使对于专家来说,多线程也难以理解.如果必须,那么使用最高级别的机制来实现您的目标.这里的解决方案不是使变量变为volatile.这里的解决方案是编写一个可取消的任务并使用任务并行库取消机制.让TPL担心让线程逻辑正确,并且正确的取消可以跨线程发送.



3> Matteo Umili..:

我附加了正在运行的进程并发现(如果我没有犯错,我对此没有很熟练)该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> InBetween..:

这不是一个真正的答案,而是为了解决这个问题:

这个问题似乎时要i声明的拉姆达体内部只有在赋值表达式读取.否则,代码在发布模式下运行良好:

    i 在lambda体外宣布:

    int i = 0; // Declared outside the lambda body
    
    var t = new Thread(() =>
    {
        while (!isComplete) { i += 0; }
    }); // Completes in release mode
    

    i 未在赋值表达式中读取:

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

    i 也在其他地方读到:

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

我的赌注是一些编译器或JIT优化有关i搞乱事情.比我更聪明的人可能会更清楚地了解这个问题.

尽管如此,我不会太担心它,因为我没有看到类似的代码在哪里实际上可以用于任何目的.

推荐阅读
黄晓敏3023
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有