Thread.VolatileRead()vs Volatile.Read()

在大多数情况下,我们被告知更喜欢Volatile.Read而不是Thread.VolatileRead ,因为后者发出全栅栏,而前者只发射相关的半栅栏(例如获取栅栏); 哪个更有效率。

但是,根据我的理解, Thread.VolatileRead实际上提供了Volatile.Read没有的东西,因为Thread.VolatileRead的实现:

 public static int VolatileRead(ref int address) { int num = address; Thread.MemoryBarrier(); return num; } 

由于实现的第二行存在完整的内存障碍,我相信VolatileRead实际上确保将读取最后写入address的值。 根据维基百科的说法, “完整的围栏确保围栏之前的所有装载和存储操作都将在围栏之后发出的任何装载和存储之前提交。”

我的理解是否正确? 因此, Thread.VolatileRead仍然提供Volatile.Read不提供的东西?

我可能会稍微迟到一些,但我还是想加入。首先我们需要就一些基本定义达成一致。

  • acquire-fence:一种内存屏障,其中不允许其他读写操作在围栏之前移动。
  • release-fence:一种内存屏障,在屏障后不允许其他读写操作。

我喜欢使用箭头符号来帮助说明行动中的栅栏。 ↑箭头表示释放栅栏,↓箭头表示获取栅栏。 把箭头想象成沿着箭头方向推开记忆存取。 但是,这很重要,内存访问可以超越尾部。 阅读上面围栏的定义,并说服自己箭头直观地表示这些定义。

使用这种表示法让我们分析JaredPar从Volatile.Read开始的答案中的例子。 但是,首先让我指出, Console.WriteLine 可能会产生一个我们不知道的全围栏障碍。 我们应该假装它不会让例子更容易理解。 事实上,我将完全忽略调用,因为在我们想要实现的目标的上下文中它是不必要的。

 // Example using Volatile.Read x = 13; var local = y; // Volatile.Read ↓ // acquire-fence z = 13; 

因此,使用箭头符号我们更容易看到写入z无法向上移动并且在读取y之前。 y的读取也不能在z的写入之后向下移动,因为这与其他方式实际上是相同的。 换句话说,它锁定了yz的相对排序。 但是, y的读取和x的写入可以交换,因为没有箭头阻止该移动。 同样,对x的写入可以移过箭头的尾部,甚至超过写入z 。 无论如何,该规范在技术上允许理论上。 这意味着我们有以下有效排序。

 Volatile.Read --------------------------------------- write x | read y | read y read y | write x | write z write z | write z | write x 

现在让我们继续使用Thread.VolatileRead进行示例。 为了示例,我将内联对Thread.VolatileRead的调用,以便更容易可视化。

 // Example using Thread.VolatileRead x = 13; var local = y; // inside Thread.VolatileRead ↑ // Thread.MemoryBarrier / release-fence ↓ // Thread.MemoryBarrier / acquire-fence z = 13; 

仔细看。 写入x和读取y之间没有箭头(因为没有内存屏障)。 这意味着这些内存访问仍然可以相对于彼此自由移动。 但是,对Thread.MemoryBarrier的调用产生了额外的release-fence,使得它看起来好像下一次内存访问具有易失性写入语义。 这意味着无法再交换对xz的写入。

 Thread.VolatileRead ----------------------- write x | read y read y | write x write z | write z 

当然,有人声称Microsoft的CLI(.NET Framework)和x86硬件的实现已经保证了所有写入的发布范围语义。 因此,在这种情况下,两个呼叫之间可能没有任何区别。 在带Mono的ARM处理器上? 在这种情况下情况可能会有所不同。

让我们继续讨论您的问题。

由于实现的第二行存在完整的内存障碍,我相信VolatileRead实际上确保将读取最后写入地址的值。 我的理解是否正确?

。 这不对! 易失性读取与“新读”不同。 为什么? 这是因为在读取指令之后放置了存储器屏障。 这意味着实际读取仍然可以自由地向上或向后移动。 另一个线程可以写入该地址,但是当前线程可能已经将读取移动到其他线程提交它之前的某个时间点。

所以这就引出了一个问题,“为什么人们在使用易失性读取时,如果看起来保证这么少呢?”。 答案是它绝对保证下一次读取比上一次读取更新 。 这是它的价值! 这就是为什么许多无锁代码在循环中旋转,直到逻辑可以确定操作成功完成。 换句话说,无锁代码利用了以下概念,即后续读取的多个读取序列将返回更新的值,但代码不应假设任何读取必须代表最新值。

想一想这一点。 读取返回最新值甚至意味着什么? 当您使用该值时,它可能不再是最新的。 另一个线程可能已经为同一地址写了不同的值。 你还能把这个价值称为最新吗?

但是,在考虑了上面讨论过的“新鲜”阅读甚至意味着什么的警告之后,你仍然想要一些像“新鲜”阅读的东西,那么你需要在阅读之前放置一个获取围栏。 请注意,这显然与易失性读取不同,但它更能匹配开发人员对“新鲜”含义的直觉。 但是,案件中的“新鲜”一词并不是绝对的。 相反,阅读相对于障碍是“新鲜的”。 也就是说,它不能超过执行障碍的时间点。 但是,如上所述,在您使用或基于它做出决定时,该值可能不代表最新值。 要时刻铭记在心。

因此,Thread.VolatileRead是否仍然提供Volatile.Read不提供的东西?

是的 我认为JaredPar提供了一个完美的例子,它可以提供额外的东西。

Volatile.Read基本上保证了在读取之前无法移动的读写操作。 它没有说明在读取之前发生的写入操作是否已经过去。 例如

 // assume x, y and z are declared x = 13; Console.WriteLine(Volatile.Read(ref y)); z = 13; 

无法保证在读取y之前写入x 。 但是,在读取y之后,保证写入z

 // assume x, y and z are declared x = 13; Console.WriteLine(Thread.VolatileRead(ref y)); z = 13; 

在这种情况下,虽然可以保证这里的订单是

  • 写x
  • 读过你
  • 写z

完整的栅栏可防止读取和写入在任一方向上移动