易失性字段:如何实际获取字段的最新写入值?
考虑以下示例:
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.Read
和Volatile.Write
。 实际上,它们甚至比volatile字段更糟糕,因为它们要求您手动执行volatile
修饰符自动执行的操作。
你是对的,不能保证所有处理器都能立即看到发布商店。 Volatile.Read
和Volatile.Write
为您提供获取/释放语义,但没有即时保证。
尽管如此, volatile
修饰符似乎也是如此。 编译器将发出OpCodes.Volatile
IL指令,抖动将告诉处理器不要将变量存储在任何寄存器上(参见Hans Passant的回答 )。
但是为什么你还需要它立即? 如果您的SecondThread
在实际写入值之前很快就会运行几毫秒怎么办? 由于调度是不确定的,因此无论如何,程序的正确性不应取决于这种“即时性”。
直到最近,我的印象是,只要FirstThread()确实在SecondThread()之前执行,该程序就无法输出除1之外的任何内容。
当你继续解释自己时,这种印象是错误的。 Volatile.Read
只是在其目标上发出一个读操作,然后是一个内存屏障; 内存屏障阻止了对执行当前线程的处理器的操作重新排序,但这在这里没有用,因为
- 没有重新排序的操作(只是每个线程中的单个读取或写入)。
- 线程中的竞争条件意味着即使在处理器之间应用了无重新排序保证,也只是意味着无法保留的操作顺序将被保留。
如果我的理解是正确的,那么如果FirstThread()中的写入尚未被释放,则没有什么可以阻止获取sharedState是“陈旧的”。
那是正确的。 从本质上讲,您使用的工具旨在帮助弱内存模型抵御由竞争条件引起的可能问题。 该工具不会帮助你,因为它不是它的function。
如果这是真的,我们怎样才能真正确保(假设最弱的处理器内存模型,如ARM或Alpha),程序将始终打印1? (或者我在某个地方的心理模型中犯了错误?)
再次强调:内存模型不是问题所在。 要确保您的程序始终打印1,您需要做两件事:
- 提供显式线程同步,保证写入将在读取之前发生(在最简单的情况下,
SecondThread
可以在FirstThread
用来发信号FirstThread
的标志上使用自旋锁)。 - 确保
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。