我遇到过这个,想知道调试和发布模式下这种行为的原因.
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
是安全的复杂程度..呃,好 ......呃,让我们说好的.
我猜这个优化器被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
是安全的复杂程度..呃,好 ......呃,让我们说好的.
quetzalcoatl的答案是正确的.为了更多地了解它:
允许C#编译器和CLR抖动进行大量优化,假设当前线程是唯一运行的线程.如果这些优化使程序在当前线程不是唯一运行的问题的线程中不正确.您需要编写多线程程序,告诉编译器和抖动您正在做的疯狂多线程的东西.
在这种特殊情况下,允许抖动 - 但不是必需的 - 观察循环体不改变变量,从而得出结论 - 因为假设这是唯一运行的线程 - 变量永远不会改变.如果它永远不会改变那么变量需要一次检查真相,而不是每次都通过循环检查.事实上,这正是发生的事情.
怎么解决这个? 不要编写多线程程序.即使对于专家来说,多线程也难以理解.如果必须,那么使用最高级别的机制来实现您的目标.这里的解决方案不是使变量变为volatile.这里的解决方案是编写一个可取消的任务并使用任务并行库取消机制.让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
声明的拉姆达体内部和它只有在赋值表达式读取.否则,代码在发布模式下运行良好:
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
搞乱事情.比我更聪明的人可能会更清楚地了解这个问题.
尽管如此,我不会太担心它,因为我没有看到类似的代码在哪里实际上可以用于任何目的.