我遇到过这个问题,想知道调试中这种行为的原因。发布模式。
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!");
}
答案 0 :(得分:150)
我猜这个优化器被缺乏“易变”' isComplete
变量上的关键字。
当然,你无法添加它,因为它是一个局部变量。当然,因为它是一个局部变量,所以根本不需要它,因为本地人保持在堆栈,并且它们自然总是"新鲜"。
然而,编译后,不再是本地变量。由于它是在匿名委托中访问的,因此代码被拆分,并被转换为辅助类和成员字段,如:
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;
}
}
我现在可以想象,处理TheHelper
类时多线程环境中的JIT编译器/优化器实际上可以缓存某些寄存器或堆栈帧中的值false
在Body()
方法的开头,并且永远不会刷新它,直到方法结束。这是因为没有保证线程和方法不会在" = true"之前结束。得到执行,所以如果没有保证,那么为什么不缓存它并获得一次读取堆对象的性能提升,而不是在每次迭代时读取它。
这正是关键字volatile
存在的原因。
对于这个帮助类,在多线程环境中正确 更好一点 1),它应该具有:< / p>
public volatile bool isComplete = false;
但是,当然,由于它是自动生成的代码,因此您无法添加它。更好的方法是在lock()
的读写操作中添加一些isCompleted
,或者使用其他一些即用型同步或线程/任务实用程序,而不是尝试裸机(不会裸机,因为它是带有GC,JIT和(..)的CLR上的C#)。
调试模式的差异可能是因为在调试模式下排除了许多优化,因此您可以调试在屏幕上看到的代码。因此,while (!isComplete)
未进行优化,因此您可以在那里设置断点,因此isComplete
不会在方法启动时在寄存器或堆栈中主动缓存,并在每次循环迭代时从堆上的对象读取
顺便说一句。这只是我对此的猜测。我甚至没有尝试编译它。
顺便说一句。它似乎不是一个bug;它更像是一种非常模糊的副作用。此外,如果我对此有所了解,那么它可能是一种语言不足 - C#应该允许放置“易变”的语言。捕获并提升到闭包中的成员字段的局部变量的关键字。
1)请参阅下文,了解Eric Lippert关于volatile
的评论和/或这篇非常有趣的文章,其中显示了确保代码依赖于volatile
所涉及的复杂程度是安全 ..uh,好 ..呃,让我们说好。
答案 1 :(得分:84)
answer of quetzalcoatl是正确的。为了更多地了解它:
允许C#编译器和CLR抖动进行大量优化,假设当前线程是唯一运行的线程。如果这些优化使得程序在当前线程不是运行这是您的问题的唯一线程的世界中不正确。您必需编写多线程程序,告诉编译器并且抖动您正在做的疯狂多线程内容。
在这种特殊情况下,允许抖动 - 但不是必需的 - 观察循环体未改变变量,从而得出结论 - 因为假设这是唯一运行的线程 - 变量将从不改变。如果它永远不会改变,则需要检查变量一次,而不是每次都通过循环。事实上,这正是发生的事情。
如何解决这个问题? 不要编写多线程程序。即使对于专家来说,多线程也难以理解。如果必须,那么使用最高级别的机制来实现目标。这里的解决方案不是使变量变为volatile。这里的解决方案是编写可取消的任务并使用任务并行库取消机制。让TPL担心让线程逻辑正确并且正确地在线程之间发送取消。
答案 2 :(得分: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
的值)并且从不刷新。
答案 3 :(得分:8)
不是一个真正的答案,而是为了解决这个问题:
问题似乎是在lambda body 中声明i
并且在赋值表达式中只读取 时。否则,代码在发布模式下运行良好:
i
:
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
我的赌注是关于i
的一些编译器或JIT优化正在弄乱事情。比我聪明的人可能会更清楚地了解这个问题。
尽管如此,我不会过分担心它,因为我没有看到类似的代码实际上可以用于任何目的。