Volatile字段:我如何才能真正地将最新的写入值写入字段



考虑以下示例:

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必须已经被获取。

如果我的理解是正确的,那么没有什么可以防止sharedState的收购是"陈旧的",如果FirstThread()的写入还没有被释放。

如果这是真的,我们如何确保(假设最弱的处理器内存模型,如ARM或Alpha),程序将始终打印1?

您的理解是正确的,您确实不能确保使用这些技术的程序总是打印1。假设线程2在线程1之后运行,为了确保程序将打印1,您需要在每个线程上设置两个fence。

最简单的方法是使用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字段。Volatile字段表明你正在做一些非常疯狂的事情:你试图在两个不同的线程上读写相同的值,而没有加锁。

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

你是对的,并不能保证所有处理器都能立即看到版本存储。Volatile.ReadVolatile.Write提供了获取/释放语义,但没有即时性保证。

volatile修饰符似乎做到了这一点。编译器将发出OpCodes.Volatile IL指令,并且抖动将告诉处理器不要将该变量存储在其任何寄存器中(参见Hans Passant的回答)。

但是你为什么需要它是即时的呢?如果您的SecondThread碰巧在实际写入值之前运行几毫秒,该怎么办?由于调度是不确定的,所以程序的正确性不应该依赖于这种"即时性"。

直到最近,我的印象是,只要FirstThread()确实在SecondThread()之前执行

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

  1. 没有重新排序的操作(每个线程只有一个读或写)。
  2. 线程间的竞争条件意味着即使在处理器间应用了无重排序保证,也仅仅意味着操作的顺序(无论如何都无法预测)将被保留。

如果我的理解是正确的,那么就没有什么如果写入,则防止sharedState被"过期"FirstThread()还没有被释放。

正确。从本质上讲,您正在使用一种旨在帮助弱内存模型应对竞争条件可能导致的问题的工具。这个工具不会帮你的,因为那不是它的功能。

如果这是真的,我们如何确保(假设最弱)处理器内存模型,如ARM或Alpha),程序将总是打印1?(或者我的思维模式犯了错误某个地方?)

再次强调:内存模型不是这里的问题。要确保你的程序总是输出1,你需要做两件事:

  1. 提供显式的线程同步,保证写入将在读取之前发生(在最简单的情况下,SecondThread可以在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之外的任何内容。

最新更新