易失性字段:如何实际获取字段的最新写入值?

考虑以下示例:

private int sharedState = 0; private void FirstThread() { Volatile.Write(ref sharedState, 1); } private void SecondThread() { int sharedStateSnapshot = Volatile.Read(ref sharedState); Console.WriteLine(sharedStateSnapshot); } 

直到最近,我的印象是,只要FirstThread()确实在SecondThread()之前执行,该程序就无法输出除1之外的任何内容。

但是,我现在的理解是:

  • Volatile.Write()发出一个发布栏。 这意味着在将1分配给sharedState之后,可能不会发生先前的加载或存储(按程序顺序)。
  • Volatile.Read()发出一个获取围栏。 这意味着在将sharedState复制到sharedStateSnapshot之前不会发生后续加载或存储(按程序顺序)。

或者,换句话说:

  • sharedState实际发布到所有处理器核心时,该写入之前的所有内容也将被释放,并且,
  • 获取地址sharedStateSnapshot的值时; 必须已经获得了sharedState

如果我的理解是正确的,那么如果FirstThread()的写入尚未被释放,则没有什么可以阻止获取sharedState是“陈旧的”。

如果这是真的,我们怎样才能真正确保(假设最弱的处理器内存模型,如ARM或Alpha),程序将始终打印1 ? (或者我在某个地方的心理模型中犯了错误?)

您的理解是正确的,并且您无法确保程序将始终使用这些技术打印1。 为了确保程序打印1,假设线程2在第一个线程之后运行,则每个线程需要两个栅栏。

实现这一目标的最简单方法是使用lock关键字:

 private int sharedState = 0; private readonly object locker = new object(); private void FirstThread() { lock (locker) { sharedState = 1; } } private void SecondThread() { int sharedStateSnapshot; lock (locker) { sharedStateSnapshot = sharedState; } Console.WriteLine(sharedStateSnapshot); } 

我想引用Eric Lippert的话 :

坦率地说,我不鼓励你做一个不稳定的领域。 易失性字段表明你正在做一些彻头彻尾的疯狂:你试图在两个不同的线程上读取和写入相同的值,而不是锁定到位。

这同样适用于调用Volatile.ReadVolatile.Write 。 实际上,它们甚至比volatile字段更糟糕,因为它们要求您手动执行volatile修饰符自动执行的操作。

你是对的,不能保证所有处理器都能立即看到发布商店。 Volatile.ReadVolatile.Write为您提供获取/释放语义,但没有即时保证。

尽管如此, volatile修饰符似乎也是如此。 编译器将发出OpCodes.Volatile IL指令,抖动将告诉处理器不要将变量存储在任何寄存器上(参见Hans Passant的回答 )。

但是为什么你还需要它立即? 如果您的SecondThread在实际写入值之前很快就会运行几毫秒怎么办? 由于调度是不确定的,因此无论如何,程序的正确性不应取决于这种“即时性”。

直到最近,我的印象是,只要FirstThread()确实在SecondThread()之前执行,该程序就无法输出除1之外的任何内容。

当你继续解释自己时,这种印象是错误的。 Volatile.Read只是在其目标上发出一个读操作,然后是一个内存屏障; 内存屏障阻止了对执行当前线程的处理器的操作重新排序,但这在这里没有用,因为

  1. 没有重新排序的操作(只是每个线程中的单个读取或写入)。
  2. 线程中的竞争条件意味着即使在处理器之间应用了无重新排序保证,也只是意味着无法保留的操作顺序将被保留。

如果我的理解是正确的,那么如果FirstThread()中的写入尚未被释放,则没有什么可以阻止获取sharedState是“陈旧的”。

那是正确的。 从本质上讲,您使用的工具旨在帮助弱内存模型抵御由竞争条件引起的可能问题。 该工具不会帮助你,因为它不是它的function。

如果这是真的,我们怎样才能真正确保(假设最弱的处理器内存模型,如ARM或Alpha),程序将始终打印1? (或者我在某个地方的心理模型中犯了错误?)

再次强调:内存模型不是问题所在。 要确保您的程序始终打印1,您需要做两件事:

  1. 提供显式线程同步,保证写入将在读取之前发生(在最简单的情况下, SecondThread可以在FirstThread用来发信号FirstThread的标志上使用自旋锁)。
  2. 确保SecondThread不会读取过时值。 你可以通过将sharedState标记为volatile来轻松地做到这一点 – 虽然这个关键字当之无愧地得到了很多瑕疵,但它是为这种用例明确设计的。

因此,在最简单的情况下,您可以例如:

 private volatile int sharedState = 0; private volatile bool spinLock = false; private void FirstThread() { sharedState = 1; // ensure lock is released after the shared state write! Volatile.Write(ref spinLock, true); } private void SecondThread() { SpinWait.SpinUntil(() => spinLock); Console.WriteLine(sharedState); } 

假设没有其他写入这两个字段,该程序保证只输出1。