.NET 中的 volatile 字段:深入探讨






4.72/5 (24投票s)
本文详细描述了 .NET 中的 volatile 字段以及它们在不同 CPU 架构上的工作方式。
引言
本文描述了 volatile 字段的内部机制。 我在网络上看到很多关于 volatile 字段的讨论。 我在这个领域进行了自己的小研究,以下是一些关于这个问题的想法。
Volatile 字段和内存屏障:深入探讨
C# volatile 字段的两个主要用途是
- 为对这些字段的所有访问操作引入内存屏障。 为了提高性能,CPU 将经常访问的对象存储在 CPU 缓存中。 在多线程应用程序的情况下,这可能会导致问题。 例如,假设一个线程一直在读取一个布尔值(读取线程),而另一个线程负责更新该字段(写入线程)。 现在,如果操作系统决定在不同的 CPU 上运行这两个线程,则更新线程可能会更改 CPU1 缓存上该字段的值,而读取线程将继续从 CPU2 缓存中读取该值。 换句话说,它将不会获得 thread1 中的更改,直到 CPU1 缓存失效。 如果两个线程更新该值,情况可能会更糟。
Volatile 字段引入内存屏障,这意味着 CPU 将始终从虚拟内存中读取和写入,而不是 CPU 缓存。 如今,诸如 x86 和 x64 之类的 CPU 架构具有 CPU 缓存一致性,这意味着一个处理器的 CPU 缓存中的任何更改都将传播到其他 CPU 缓存。 并且,反过来,这意味着 x86 和 x64 平台的 JIT 编译器对 volatile 和非 volatile 字段没有区别(除了项目 #2 中所述的那个)。 此外,多核 CPU 通常有两个级别的缓存:第一级在 CPU 内核之间共享,第二级则不共享。 但是,诸如 Itanium 之类的具有弱内存模型的 CPU 架构不具有缓存一致性,因此在设计多线程应用程序时,
volatile
关键字和内存屏障起着重要作用。 因此,我建议即使对于 x86 和 x64 CPU 也始终使用volatile
和内存屏障,因为否则,您将为您的应用程序引入 CPU 架构亲和性。注意:您还可以通过使用
Thread.VolatileRead
/Thread.VolatileWrite
(这两个方法成功替换了volatile
关键字)、Thread.MemoryBarrier
甚至使用 C#lock
关键字等来引入内存屏障。下面显示了两种 CPU 架构:Itanium 和 AMD(Direct Connect 架构)。 我们可以看到,在 AMD 的 Direct Connect 架构中,所有处理器都相互连接,因此我们具有内存一致性。 在 Itanium 架构中,CPU 彼此不连接,它们通过系统总线与 RAM 通信。
- 防止指令重新排序。 例如,假设我们有一个循环
while(true) { if(myField) { //do something } }
在非 volatile 字段的情况下,由于性能方面的考虑,在 JIT 编译期间,JIT 编译器可以按以下方式重新排序指令
if(myField) { while(true) { //do something } }
如果您计划从单独的线程更改
myField
,会有什么显着差异吗? 通常,建议使用lock
语句 (Monitor.Enter
或Monitor.Exit
),但是如果您在此块中仅更改一个字段,则 volatile 字段的性能将明显优于Monitor
类。
特别感谢
我要感谢来自 Microsoft 的 Stefan Repas 提供了有关此主题的一些有用的信息。