Thread.VolatileRead()vs Volatile.Read()

Xen*_*ate 29 .net c# multithreading

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

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

public static int VolatileRead(ref int address) {
  int num = address;
  Thread.MemoryBarrier();
  return num;
}
Run Code Online (Sandbox Code Playgroud)

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

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

Bri*_*eon 35

我可能会比较晚一点,但我仍然想要进入.首先我们需要就一些基本定义达成一致.

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

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

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

// Example using Volatile.Read
x = 13;
var local = y; // Volatile.Read
?              // acquire-fence
z = 13;
Run Code Online (Sandbox Code Playgroud)

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

Volatile.Read
---------------------------------------
write x    |    read y     |    read y
read y     |    write x    |    write z
write z    |    write z    |    write x
Run Code Online (Sandbox Code Playgroud)

现在让我们继续讨论这个例子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;
Run Code Online (Sandbox Code Playgroud)

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

Thread.VolatileRead
-----------------------
write x    |    read y
read y     |    write x
write z    |    write z
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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

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

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

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

  • 每当我想出一个拥有线程安全变量的最佳方法时,我会发现一些新的*叹气*.这种多线程的东西真的很烦人...... (2认同)

Jar*_*Par 16

Volatile.Read本质上保证了读取和写入后不能读取之前移动时产生的操作.它没有提到阻止在读取之前发生的写入操作.例如

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

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

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

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

  • 写x
  • 写z

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

  • 我不认为这是完全正确的.在你的第二个例子中,写入x和来自Y的VolatileRead之间没有内存屏障(实现显示读取发生在内存屏障之前)所以仍然可能x和y可以重新排序.也许我错了,这都是非常令人困惑的事情 (3认同)