Thread.VolatileRead实现

25 c# multithreading memory-model

我正在研究VolatileRead/VolatileWrite方法的实现(使用Reflector),我对此感到困惑.

这是VolatileRead的实现:

[MethodImpl(MethodImplOptions.NoInlining)]
public static int VolatileRead(ref int address)
{
    int num = address;
    MemoryBarrier();
    return num;
}
Run Code Online (Sandbox Code Playgroud)

在读取"地址"的值后,如何放置内存屏障?不应该是相反的吗?(在读取值之前放置,所以对于"address"的任何挂起写入都将在我们进行实际读取时完成.同样的事情发生在VolatileWrite,其中内存屏障在赋值之前放置.为什么?另外,为什么这些方法具有NoInlining属性?如果它们被内联会发生什么?

Jon*_*eet 24

我想到了最近.易失性读取不是你认为的那样 - 它们不是要保证它们获得最新的价值; 他们关于确保没有读这就是后来在程序代码被前移到读.这就是规范所保证的 - 同样对于易失性写入,它保证在volatile 之后不会移动先前的写入.

并不是唯一一个怀疑这段代码的人,但是Joe Duffy解释得比我更好 :)

我对此的回答是放弃无锁编码,而不是使用像PFX这样的东西,这些东西旨在使我免受干扰.记忆模型对我来说太难了 - 我会留给专家,并坚持我认为安全的事情.

有一天,我会更新我的线程文章以反映这一点,但我认为我需要能够更明智地讨论它...

(我不知道没有内联的部分,顺便说一下.我怀疑内联可能会引入一些其他的优化,这些优化不会发生在易失性读/写操作中,但我可能很容易出错...)

  • @Jon:根据我的理解,保证是如果thread1写了一些东西,然后将VolatileWrite写入SomeFlag,并且如果thread2执行SomeFlag的VolatileRead并且发现它的设置方式只能从thread1发生, thread2可以确保对thread1的VolatileWrite之前最后写入的变量的任何后续读取都将反映最后写入的值. (5认同)
  • @opc:我认为基本的一点是,保证你之后阅读的任何内容至少都是新鲜的.因此,如果以正确的顺序进行读写,您可以做出明智的决定.*可能*通过新鲜读取/写入来实现内存屏障 - 但不是由规范保证:( (2认同)
  • @unknown:不可能(永远)保证你"获取最后一个值...写入[a]变量",因为你通常无法操作任何位而不将它们加载到线程的CPU上下文中,并且总是可以有人在你"拥有"它之后(在你的上下文中),但在你使用它之前改变它们.最接近的是CompareExchange,它允许您至少放弃操作和/或检测何时发生. (2认同)

小智 5

也许我过于简单化了,但我认为有关重新排序和缓存一致性等的解释给出了太多细节。

那么,为什么 MemoryBarrier 出现在实际读取之后呢?我将尝试用一个使用 object 而不是 int 的示例来解释这一点。

人们可能认为正确的是:线程 1 创建对象(初始化其内部数据)。然后线程 1 将对象放入变量中。然后它“做一个栅栏”并且所有线程都会看到新值。

然后,读取内容如下:线程 2“做了一个栅栏”。线程 2 读取对象实例。线程 2 确保它拥有该实例的所有内部数据(因为它以栅栏开始)。

最大的问题是:线程 1 创建对象并初始化它。然后线程 1 将对象放入变量中。在线程刷新缓存之前,CPU 本身会刷新缓存的一部分...它仅提交变量的地址(而不是该变量的内容)。

此时,线程 2 已经刷新了其缓存。所以它将从主存储器中读取所有内容。因此,它读取变量(它就在那里)。然后它读取内容(它不在那里)。

最后,在这一切之后,CPU 1 执行执行栅栏的线程 1。


那么,易失性写入和读取会发生什么情况?易失性写入使对象的内容立即进入内存(从栅栏开始),然后设置变量(可能不会立即进入实际内存)。然后,易失性读取将首先清除缓存。然后它读取该字段。如果它在读取字段时收到一个值,则可以肯定该引用指向的内容确实存在。


通过这些小事情,是的,您可能执行 VolatileWrite(1) 而另一个线程仍然看到零值。但是,一旦其他线程看到值 1(使用易失性读取),可能引用的所有其他所需项目都已存在。你不能真正告诉它,因为当读取旧值(0 或 null)时,考虑到你还没有拥有你需要的一切,你可能简单地没有进展。


我已经看到一些讨论,即使刷新缓存两次,正确的模式将是:
MemoryBarrier - 将刷新在此调用之前更改的其他变量
Write
MemoryBarrier - 将保证写入被刷新

然后,读取将需要相同的内容:
MemoryBarrier
Read - 保证我们看到最新的信息......也许是在我们的内存屏障之后放置的信息。
由于某些内容可能出现在我们的 MemoryBarrier 之后并且已经被读取,因此我们必须放置另一个 MemoryBarrier 来访问内容。

如果 .Net 中存在的话,它们可能是两个 Write-Fence 或两个 Read-Fence。


我不确定我所说的一切......这是我得到的许多信息的“编译”,它确实解释了为什么 VolatileRead 和 VolatileWrite 看起来是相反的,但它也保证在使用它们时不会读取无效值。