Volatile.Read 和 Volatile.Write 背后的逻辑是什么?

Lif*_*ang 6 .net c# parallel-processing concurrency multithreading

从MSDN, Volatile.Read()

读取字段的值。在需要它的系统上,插入一个内存屏障,以防止处理器重新排序内存操作,如下所示:如果读取或写入出现在代码中此方法之后,则处理器无法将其移动到此方法之前

Volatile.Write()

将值写入字段。在需要它的系统上,插入一个内存屏障,以防止处理器重新排序内存操作,如下所示:如果读取或写入出现在代码中的此方法之前,则处理器无法此方法之后移动它。

我想我能理解Volatile.Read()and的使用场景Volatile.Write(),并且看到很多例子解释为什么这两种方法有助于确保程序的正确性。

但我还是想知道,这些规则背后的逻辑是什么?

Volatile.Read()为例,为什么它要求操作后,它不能移动之前,但不需要来自任何操作之前呢?

还有为什么它与Volatile.Write()?

谢谢!

Mat*_*ans 7

对 volatile 读和 volatile 写的保证确保如果一个线程使用 volatile 写来指示某事已完成,然后另一个线程使用 volatile 读来通知某事已完成,那么第二个线程将看到那东西。

例如,假设Thread1初始化 object A,然后对 a 进行 volatile 写入,flag表明它已完成。初始化对象字段所涉及的所有内存操作都A发生在代码中的标志设置之前。保证是这些“在易失性写入后无法移动”到flag,因此当标志在内存中设置时,整个初始化对象都在内存中,其他线程可以看到它。

现在让我们说Thread2正在等待那个对象。它有一个 volatile 读取,可以看到flag设置,然后读取字段A并根据读取的内容做出决定。这些读操作发生在代码中的 volatile 读之后,而 volatile 读保证它们会在内存中的 volatile 读之后发生,这样就Thread2可以保证看到完全初始化的 object 字段A,而不是它之前存在的任何内容。

所以:Thread1在 volatile 写入之前确实进入内存的写入flag,这显然必须在Thread2volatile 读取它之前进入内存,并且在此之后Thread2发生以下读取,因此它看到正确初始化的对象。

这就是为什么写入不能延迟到 volatile 写入之后,并且读取不能在 volatile 读取之前向上移动。反过来呢?

好吧,让我们说Thread2,在它看到A初始化后,做一些工作并将其写入一些Thread1用于决定如何初始化的内存A。这些写入保证不会在内存中出现,直到 Thread2看到的是A正在做的,而读取Thread1品牌到这些位置都保证发生之前flag在内存设置,那么Thread2的写保证不与初始化工作造成干扰。


Val*_*rov 7

这些规则背后的逻辑称为内存模型。
在.NET中,我们的内存模型相当弱(参见ECMA-335),这意味着编译器、jit和cpu可以进行大量优化(只要它们保持单线程语义和易失性语义),这在优化的可能性。
只要满足以下条件,编译器/jit/cpu就可以进行任何优化:

CLI 的一致实现可以自由地使用任何技术来执行程序,只要保证在单个执行线程内,线程生成的副作用和异常按照 CIL 指定的顺序可见。为此目的,只有易失性操作(包括易失性读取)才会产生可见的副作用。(请注意,虽然只有易失性操作才会产生可见的副作用,但易失性操作也会影响非易失性引用的可见性。)

这意味着除非您使用隐式或显式易失性操作,否则所有代码都被假定为单线程。
例如,

获取锁(System.Threading.Monitor.Enter或进入同步方法)应隐式执行易失性读操作,释放锁(System.Threading.Monitor.Exit或离开同步方法)应隐式执行易失性写操作。

这意味着不可能将任何操作(来自 lock 语句)移至上方(隐式 Volatile.Read 阻止此操作),并且不可能将它们移至锁下方(隐式 Volatile.Write 阻止此操作)。因此它们保留在锁定语句内,但仍然可以在此锁定语句内重新排序或优化它们。