【问题标题】:.NET JIT compiler volatile optimizations.NET JIT 编译器易失性优化
【发布时间】:2019-03-06 14:00:18
【问题描述】:

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 字段是可变的,JIT 编译器不会 将读数提升到循环之外。 (参见“轮询循环”部分 12 月的文章可以更详细地解释这种模式。)

如果我锁定_flag,JIT 编译器是否仍会优化代码,还是只会让它volatile 停止优化?

Eric Lippert 对 volatile 有以下看法:

坦率地说,我不鼓励你创建一个不稳定的字段。易挥发的 字段表明你正在做一些非常疯狂的事情:你是 试图在两个不同的线程上读取和写入相同的值 没有把锁到位。锁保证内存读取或 修改里面的锁观察是一致的,锁保证 一次只有一个线程访问给定的内存块,并且 很快。锁太慢的情况非常多 小,而且你会弄错代码的可能性 因为你不了解确切的内存模型非常大。一世 不要尝试编写任何低锁代码,除了最琐碎的代码 互锁操作的用法。我将“易失性”的用法留给 真正的专家。

总结一下:谁保证上面提到的优化不会破坏我的代码?只有volatile?还有lock 声明?还是别的什么?

由于 Eric Lippert 不鼓励您使用 volatile,肯定还有其他原因?


投反对票的人:我感谢对这个问题的每一个反馈。特别是如果你不赞成,我想听听你为什么认为这是一个糟糕的问题。


bool 变量不是线程同步原语:该问题是一般问题。编译器什么时候不做优化?


重复:这个问题明确与优化有关。您链接的那个没有提到优化。

【问题讨论】:

  • @Mgetz 嘿,感谢您的评论。我既不是专门寻找联锁操作也不是取消令牌。我只想知道在这种特定情况下如何停止上述优化。用锁包裹字段会起作用吗?还有什么?优化后如何防止代码中断/防止优化?
  • 互锁操作是一种特殊情况,它告诉编译器非常特殊地对待它们。您实际上是在告诉编译器您需要它来处理该内存位置,就好像它可能随时更改一样。作为回报,您只会使用特殊的访问方式(互锁)。然后它可以适当地围绕它进行优化
  • bool 变量不是线程同步原语,永远不会。当您正确执行此操作时,您永远不必担心 Microsoft 弄错 volatile 关键字的确切方式。 ManualResetEventSlim 是 Interlocked 的一个很好的包装器,您可以通过在 if 语句中使用 Volatile.Read() 和 Volatile.Write() 来设置它来使 bool 工作。只要不忽略异常,Task 和 CancellationToken 就可以提高抽象级别,几乎没有缺点。
  • @Mgetz 这个问题明确与优化有关。您链接的那个没有提到优化。
  • @Mgetz 是的,谢谢。我现在明白了。这就是为什么我在你的句子中附加了“当场没有互锁时”。如果有人将其发布为答案,我会接受。更多细节总是好的。

标签: c# .net multithreading volatile jit


【解决方案1】:

让我们回答被问到的问题:

如果我锁定 _flag,JIT 编译器是否仍会优化代码,还是只会使其 volatile 停止优化?

好的,我们不回答被问到的问题,因为这个问题太复杂了。让我们把它分解成一系列不太复杂的问题。

如果我锁定 _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;
} 

(当然,我注意到这是等待一个线程向另一个线程发出信号的非常糟糕的方式。永远不要坐在一个紧密的循环中轮询标志!使用等待句柄像一个理智的人。)

你说锁比 volatile 更强大;这是什么意思?

对 volatile 的读取会阻止某些操作及时移动。写入 volatile 会阻止某些操作及时移动。锁会阻止 more 操作及时移动。这些预防语义被称为“内存栅栏”——基本上,易失性引入了半栅栏,锁引入了完整的栅栏。

阅读有关特殊副作用的 C# 规范部分了解详细信息。

与往常一样,我会提醒您,volatile 不会为您提供全球新鲜度保证。在多线程 C# 编程中没有变量的“最新”值这样的东西,因此 volatile 读取不会给您“最新”值,因为它不存在。存在“最新”值的想法意味着始终观察到读取和写入具有全局一致的时间顺序,这是错误的。线程仍然可以在 volatile 读取和写入的顺序上存在分歧。

锁阻止了这种优化; volatiles 阻止了这种优化。这些是唯一阻碍优化的东西吗?

没有。您也可以使用Interlocked 操作,也可以显式引入内存围栏。

我是否足够了解这一点以正确使用volatile

没有。

我该怎么办?

首先不要编写多线程程序。一个程序中的多个控制线程是个坏主意。

如果必须,请不要跨线程共享内存。将线程用作低成本进程,并且仅在您拥有可以执行 CPU 密集型任务的空闲 CPU 时使用它们。对所有 I/O 操作使用单线程异步。

如果您必须跨线程共享内存,请使用可供您使用的最高级别的编程构造,而不是最低级别。使用CancellationToken 表示在异步工作流的其他地方取消的操作。

【讨论】:

  • 救世主来了。非常感谢您的时间!
  • 所以基本上只有.NET Framework开发者需要使用volatile?并且只需要在框架中创建下一个更高级别的组件,如Monitor 等等?
  • @NtFreX:是的,我的建议是只使用volatile 来构建更高级别的组件。如果要延迟初始化,请使用Lazy<T>。它在内部使用 volatile,它是由知道其含义的专家编写的。如果要取消,请使用取消令牌。如果您想要等待,请使用等待句柄。不要自己动手。
  • 不,你永远不想使用多个线程来做文件 IO,这就是我要说的。将文件 IO 想象成在邮件中发送一封信,然后在两周后收到回复。 在这种情况下,无论您发送和接收多少封信,您都无需雇用任何人为您发送或接收您的信件。您雇用某人坐在您的邮箱旁不会使邮政服务更快或更高效
  • IO 由 hardware 执行,它独立于 CPU 和任何操作系统级别的概念(如“线程”)而存在。 CPU 和硬件使用中断相互通信。对 SO 问题的评论不是为您提供有关硬件如何工作的教程的好地方;做一些研究,如果你还有问题,问一个新问题
【解决方案2】:

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

【讨论】:

  • 我在问题下做了一些cmets。 lock 和 Interlocked 不表示数据存在数据竞争。他们要求一个非常具体的行为。它们不是禁用优化的开关。例如,JIT 仍然可以将volatileField = 1; volatileField = 2; 转换为volatileField = 2; 并对其进行优化。
  • @usr 虽然我没有提到我知道。要求是向编译器指示数据竞争,这就是问题所在。
【解决方案3】:

这个问题明确是关于优化的

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


您无法锁定_flaglockMonitor 类的语法。该类基于堆上的对象锁定。 _flag 是一个不可锁定的布尔值。

为了取消循环,这些天我会使用CancellationTokenSource。它在内部使用 volatile 访问,但对您隐藏。循环轮询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 说得非常正确,如果没有必要,最好不要接触硬核线程。

【讨论】:

  • volatile 不应该用于线程安全,如果您需要跨线程更改原语,请使用 Interlocked operations
  • @Mgetz 我不同意。在 .NET 中,volatile 定义明确,可以使用。它对应于Volatile 类。也许你想起了 C++ volatile,据我了解,它是不可用的。
  • 鉴于 volatile 的保证不一致以及难以解释如何正确使用它...我鼓励使用库提供的显式原子操作。
  • @NtFrX 是的。锁包含强制更新对其他线程可见的内存屏障。
  • @NtFreX 错误行为取决于非常具体的情况。有时它看起来是随机的,有时它是确定性错误的,有时它是完全正确的,直到您的寻呼机在凌晨 4 点响起。
【解决方案4】:

C# volatile 关键字实现了所谓的获取和释放语义,因此使用它来进行简单的线程同步是完全合法的。并且任何符合标准的 JIT 引擎都不应该优化它。

当然,这是一种虚假的语言特性,因为 C/C++ 具有不同的语义,而这是大多数程序员可能已经习惯的。因此,“volatile”的特定于 C# 和特定于 Windows(ARM 架构除外)的用法有时会令人困惑。

【讨论】:

    猜你喜欢
    • 2012-12-19
    • 2010-09-21
    • 2015-09-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-09
    相关资源
    最近更新 更多