65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.72/5 (24投票s)

2008 年 11 月 27 日

CPOL

3分钟阅读

viewsIcon

49089

本文详细描述了 .NET 中的 volatile 字段以及它们在不同 CPU 架构上的工作方式。

引言

本文描述了 volatile 字段的内部机制。 我在网络上看到很多关于 volatile 字段的讨论。 我在这个领域进行了自己的小研究,以下是一些关于这个问题的想法。

Volatile 字段和内存屏障:深入探讨

C# volatile 字段的两个主要用途是

  1. 为对这些字段的所有访问操作引入内存屏障。 为了提高性能,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 通信。

  2. 防止指令重新排序。 例如,假设我们有一个循环
    while(true)
    {
       if(myField)
       {
          //do something
       }
    }

    在非 volatile 字段的情况下,由于性能方面的考虑,在 JIT 编译期间,JIT 编译器可以按以下方式重新排序指令

    if(myField)
    {
       while(true)
       {
          //do something
       }
    }

    如果您计划从单独的线程更改 myField,会有什么显着差异吗? 通常,建议使用 lock 语句 (Monitor.EnterMonitor.Exit),但是如果您在此块中仅更改一个字段,则 volatile 字段的性能将明显优于 Monitor 类。

特别感谢

我要感谢来自 Microsoft 的 Stefan Repas 提供了有关此主题的一些有用的信息。

© . All rights reserved.