https://msdn.microsoft.com/en-us/magazine/jj883956.aspx
考虑轮询循环模式:
private bool _flag = true; public void Run() { // Set _flag to false on another thread new Thread(() => { _flag = false; }).Start(); // Poll the _flag field until it is set to false while (_flag) ; // The loop might never terminate! }
在这种情况下,.NET 4.5 JIT编译器可能会这样重写循环:
if (_flag) { while (true); }
在单线程情况下, 转型是完全合法的,通常来说, 循环的优化是非常好的。但是,如果设置了_flag 如果在另一个线程上设置为false,则优化可能会导致挂起。
请注意,如果_flag字段是volatile,则JIT编译器不会 提升读取的循环。 (请参见 12月的文章中对此模式进行了更详细的说明。)
如果我锁定_flag
或仅使其volatile
停止优化,JIT编译器是否仍将如上所述优化代码?
埃里克·利珀特(Eric Lippert)关于挥发物的说法如下:
坦率地说,我不鼓励您从事不稳定的领域。易挥发的 字段表明您正在疯狂地做某事:您正在 尝试在两个不同的线程上读取和写入相同的值 没有锁定的位置。锁可确保读取或读取内存 观察到锁内部的修改是一致的,锁保证 一次只有一个线程访问给定的内存块,并且 以此类推。锁太慢的情况非常多 很小,并且您可能会弄错代码的可能性 因为您不了解确切的内存模型非常大。一世 除最琐碎的代码外,请勿尝试编写任何低锁代码 互锁操作的用法。我将“ volatile”的用法留给 真正的专家。
总结:谁能确保上述优化不会破坏我的代码?只有volatile
?还有lock
语句吗?还是其他?
当埃里克·利珀特(Eric Lippert)阻止您使用volatile
时,还必须有其他东西吗?
拒绝投票者:感谢您对此问题的所有反馈。特别是如果您投票否决,我想听听您为什么认为这是一个不好的问题。
bool变量不是线程同步原语:这个问题是一个Generell问题。编译器什么时候不做优化?
Dupilcate::该问题明确涉及优化。您链接的链接没有提到优化。
答案 0 :(得分:11)
让我们回答所提出的问题:
如果我锁定_flag或仅使其变得易失,JIT编译器是否仍将如上所述优化代码?
好的,我们不要回答这个问题,因为这个问题太复杂了。让我们将其分解为一系列不太复杂的问题。
如果我锁定_flag,JIT编译器是否仍将如上所述优化代码?
简短的回答:lock
比volatile
提供了一个更强的保证,所以不,如果存在以下情况,将不允许抖动将读数提升到循环之外:锁定_flag
的读取。当然,锁也必须位于写操作附近。锁只有在无处不在的情况下才能使用。
private bool _flag = true;
private object _flagLock = new object();
public void Run()
{
new Thread(() => { lock(_flaglock) _flag = false; }).Start();
while (true)
lock (_flaglock)
if (!_flag)
break;
}
(当然,我注意到,这是一种等待一个线程向另一个线程发出信号的非常糟糕的方法。永远不要坐在轮询标志的紧密循环中!请使用等待句柄像个明智的人。)
您说锁比挥发物强;是什么意思?
读取易失性可防止某些操作随时间移动。写入易失性可防止某些操作随时间移动。锁可防止 more 操作及时移动。这些预防语义被称为“内存围栏”-基本上,挥发物引入了半围栏,锁引入了全围栏。
有关特殊副作用的详细信息,请阅读C#规范部分。
一如既往,我会提醒您,挥发物不会为您提供全局的新鲜度保证。在多线程C#编程中,没有变量的“最新”值,因此易失性读取不会为您提供“最新”值,因为它不存在。存在“最新”值的想法意味着始终观察到读写在时间上具有全局一致的顺序,这是错误的。线程仍然可以在易失性读写顺序上发生分歧。
锁阻止此优化;挥发物阻止了这种优化。是唯一阻止优化的事情吗?
不。您还可以使用Interlocked
操作,也可以显式引入内存防护。
我是否足够理解以正确使用
volatile
?
不。
我该怎么办?
首先不要编写多线程程序。一个程序中有多个控制线程是一个坏主意。
如果必须的话,不要跨线程共享内存。将线程用作低成本进程,并且仅在闲置的CPU可以执行CPU密集型任务时才使用它们。对所有I / O操作使用单线程异步。
如果必须在线程之间共享内存,请使用可用的最高级别的编程结构,而不要使用最低级别的。使用CancellationToken
表示在异步工作流中其他位置被取消的操作。
答案 1 :(得分:2)
使用lock
或Interlocked Operations时,您是在告诉编译器块或内存位置存在数据争用,并且不能对访问这些位置做出假设。因此,编译器放弃了可以在无数据争用环境中执行的优化。此隐含合同还意味着您要告诉编译器,您将以适当的无数据争用的方式访问这些位置。
答案 2 :(得分:1)
这个问题明确地关于优化
这不是正确的观点。重要的是如何指定语言的行为。 JIT将仅在不违反规范的约束下进行优化。因此,优化对于程序是不可见的。这个问题中的代码的问题不在于它正在被优化。问题是规范中没有任何内容强制程序正确。为了解决此问题,您不关闭优化或以某种方式与编译器通信。您使用可以保证所需行为的原语。
您无法锁定_flag
。 lock
是Monitor
类的语法。该类基于堆上的对象锁定。 _flag
是不可锁定的布尔值。
这几天,我将使用CancellationTokenSource
来取消循环。它在内部使用易失性访问,但对您隐藏。循环轮询CancellationToken
,并通过调用CancellationTokenSource.Cancel()
来取消循环。这是非常自我记录,易于实现。
您还可以将对_flag
的所有访问都包装在一个锁中。看起来像这样:
object lockObj = new object(); //need any heap object to lock
...
while (true) {
lock (lockObj) {
if (_flag) break;
}
...
}
...
lock (lockObj) _flag = true;
您也可以使用volatile
。埃里克·利珀特(Eric Lippert)非常正确,如果不需要,最好不要接触硬核线程的东西。
答案 3 :(得分:0)
C#volatile关键字实现了所谓的获取和释放语义,因此使用它进行简单的线程同步是完全合法的。而且任何符合标准的JIT引擎都不应对其进行优化。
当然,这是一种伪造的语言功能,因为C / C ++具有不同的语义,而这正是大多数程序员可能已经习惯的。因此,“ volatile”的特定于C#和Windows的特定用法(ARM体系结构除外)有时会造成混淆。