【问题标题】:Thread.VolatileRead ImplementationThread.VolatileRead 实现
【发布时间】:2009-11-20 22:43:17
【问题描述】:

我正在查看 VolatileRead/VolatileWrite 方法的实现(使用 Reflector),我对某些事情感到困惑。

这是 VolatileRead 的实现:

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

读取“地址”的值后,内存屏障是怎么设置的?不应该是相反的吗? (放置在读取值之前,因此任何对“地址”的待处理写入都将在我们进行实际读取时完成。 VolatileWrite 也是如此,其中内存屏障位于值的分配之前。这是为什么? 另外,为什么这些方法有 NoInlining 属性?如果它们被内联会发生什么?

【问题讨论】:

  • +1。很有趣的问题

标签: c# multithreading memory-model


【解决方案1】:

直到最近我才这么认为。易失性读取并不是您认为的那样——它们不是保证它们获得最新的值;他们的目的是确保在 this 读取之前不会将程序代码中稍后的读取移动到。这就是规范所保证的 - 同样对于易失性写入,它保证不会将更早的写入移动到 易失性写入之后。

您是 not alone 怀疑此代码,但 Joe Duffy explains it better than I can :)

我对此的回答是放弃无锁编码,而不是使用 PFX 之类的东西来隔离我。记忆模型对我来说太难了 - 我会把它留给专家,并坚持我知道安全的东西。

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

(顺便说一句,我不知道非内联部分。我怀疑内联可能会引入一些其他优化,这些优化并不意味着围绕易失性读/写发生,但我很容易错...)

【讨论】:

  • opc:严格来说不是,不是(尽管 MSDN 是这么说的)——尽管我相信几乎在任何实际情况下,您实际上得到最后一个值。
  • MSDN 说:“VolatileWrite 确保写入内存位置的值立即对所有处理器可见。这可能需要刷新处理器缓存”,因此“刷新缓存”由用户?因为我们可以看到调用 VolatileWrite 不会等到存储缓冲区为空 .. 这意味着 MSDN 中的文档完全错误且具有误导性 .. 或者我在这里错了?
  • @opc:我认为基本的一点是保证您之后阅读的任何其他内容至少是新鲜的。因此,如果您以正确的顺序进行读取和写入,您就可以做出明智的决定。 可能内存屏障是通过新读/写精确实现的 - 但规范不能保证:(
  • @Jon:据我了解,保证是如果 thread1 写入一些东西,然后对 SomeFlag 执行 VolatileWrite,如果 thread2 执行 SomeFlag 的 VolatileRead 并看到它已设置在只能从线程 1 发生的方式,线程 2 可以确保对在线程 1 的 VolatileWrite 之前最后写入的变量的任何后续读取都将反映最后写入的值。
  • @unknown:不可能(永远)保证您将“获得最后一个值......写入 [a] 变量”,因为您通常无法操作任何位而不将它们加载到您的线程的CPU 上下文,并且在你“拥有”它之后(在你的上下文中),但在你使用它之前,某人总是有可能改变它们。您可以获得的最接近的是 CompareExchange,它至少允许您放弃操作和/或检测它何时发生。
【解决方案2】:

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

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

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

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

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

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

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


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


通过这些小事,是的,有可能您执行了 VolatileWrite(1) 并且另一个线程仍然看到零值。但是一旦其他线程看到 1 的值(使用 volatile 读取),所有其他可能被引用的项目都已经存在。您无法真正告诉它,因为在读取旧值(0 或 null)时,考虑到您仍然没有所需的一切,您可能根本没有进步。


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

MemoryBarrier - 将保证写入已刷新

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

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


我不确定我所说的一切...这是对我获得的许多信息的“汇编”,它确实解释了为什么 VolatileRead 和 VolatileWrite 似乎被颠倒了,但它也保证在以下情况下不会读取无效值使用它们。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-10-19
    • 2020-01-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多