Thread.VolatileRead实现

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

这是VolatileRead的实现:

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

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

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

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

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

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

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

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

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

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

然后,读取是这样的:线程2“做栅栏”。 线程2读取对象实例。 线程2确保它具有该实例的所有内部数据(因为它以栅栏开始)。

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

那一刻,线程2已经刷新了它的缓存。 所以它将从主内存中读取所有内容。 所以,它读取变量(它就在那里)。 然后它读取内容(它不存在)。

最后,在所有这些之后,CPU 1执行执行栅栏的线程1。


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


通过这些小事,是的,你可以做一个VolatileWrite(1),另一个线程仍然可以看到零值。 但是,一旦其他线程看到值1(使用易失性读取),可能引用的所有其他项目已经存在。 你不能真正告诉它,当读取旧值(0或null)时,考虑到你还没有所需的一切,你可能很容易就没有进展。


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

MemoryBarrier – 将保证写入被刷新

Read将需要相同的:
内存屏障
读 – 保证我们看到最新的信息…可能是我们的记忆障碍之后的一个。
由于某些内容可能已经出现在我们的MemoryBarrier之后并且已经被读取,我们必须使用另一个MemoryBarrier来访问内容。

那些可能是两个写围栏或两个读围栏,如果它存在于.Net中。


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