.NET JIT编译器易失性优化

时间:2018-10-01 12:44:01

标签: c# .net multithreading volatile jit

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::该问题明确涉及优化。您链接的链接没有提到优化。

4 个答案:

答案 0 :(得分:11)

让我们回答所提出的问题:

  

如果我锁定_flag或仅使其变得易失,JIT编译器是否仍将如上所述优化代码?

好的,我们不要回答这个问题,因为这个问题太复杂了。让我们将其分解为一系列不太复杂的问题。

  

如果我锁定_flag,JIT编译器是否仍将如上所述优化代码?

简短的回答:lockvolatile提供了一个更强的保证,所以不,如果存在以下情况,将不允许抖动将读数提升到循环之外:锁定_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)

使用lockInterlocked Operations时,您是在告诉编译器块或内存位置存在数据争用,并且不能对访问这些位置做出假设。因此,编译器放弃了可以在无数据争用环境中执行的优化。此隐含合同还意味着您要告诉编译器,您将以适当的无数据争用的方式访问这些位置。

答案 2 :(得分:1)

  

这个问题明确地关于优化

这不是正确的观点。重要的是如何指定语言的行为。 JIT将仅在不违反规范的约束下进行优化。因此,优化对于程序是不可见的。这个问题中的代码的问题不在于它正在被优化。问题是规范中没有任何内容强制程序正确。为了解决此问题,您关闭优化或以某种方式与编译器通信。您使用可以保证所需行为的原语。


您无法锁定_flaglockMonitor类的语法。该类基于堆上的对象锁定。 _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体系结构除外)有时会造成混淆。