Thread.VolatileRead()vs Volatile.Read()

时间:2014-03-21 20:49:24

标签: c# .net 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;
}

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

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

2 个答案:

答案 0 :(得分:35)

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

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

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

使用此表示法,让我们分析JaredPar's answer中以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的调用产生了额外的释放栅栏,使得它看起来好像下一个内存访问具有易失性写入语义。这意味着无法再交换xz的写入内容。

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

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

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

  

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

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

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

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

但是,在考虑了什么甚至意味着拥有一个"新鲜"阅读上面讨论的内容,你仍然想要一些像'" fresh"然后你需要在阅读之前放置一个获取栅栏。请注意,这显然与易失性读取不同,但它更能匹配开发人员对什么" fresh"手段。然而,术语"新鲜"在这种情况下不是绝对的。相反,阅读是#34;新鲜"相对于障碍。也就是说,它不能超过执行障碍的时间点。但是,如上所述,在您使用或基于它做出决定时,该值可能不代表最新值。请记住这一点。

  

因此,Thread.VolatileRead仍然提供了一些东西   Volatile.Read没有?

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

答案 1 :(得分:16)

Volatile.Read基本上保证了在读取之后无法移动的读写操作。它没有提到阻止在读取之前发生的写入操作。例如

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

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

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

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

  • 写x
  • 读y
  • 写z

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