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这样的东西,这些东西旨在使我免受干扰.记忆模型对我来说太难了 - 我会留给专家,并坚持我认为安全的事情.
有一天,我会更新我的线程文章以反映这一点,但我认为我需要能够更明智地讨论它...
(我不知道没有内联的部分,顺便说一下.我怀疑内联可能会引入一些其他的优化,这些优化不会发生在易失性读/写操作中,但我可能很容易出错...)
小智 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 看起来是相反的,但它也保证在使用它们时不会读取无效值。