首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >Thread.VolatileRead()与Volatile.Read()

Thread.VolatileRead()与Volatile.Read()
EN

Stack Overflow用户
提问于 2014-03-21 20:49:23
回答 2查看 6.4K关注 0票数 33

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

然而,以我的理解,Thread.VolatileRead实际上提供了一些Volatile.Read没有提供的东西,因为Thread.VolatileRead的实现

代码语言:javascript
复制
public static int VolatileRead(ref int address) {
  int num = address;
  Thread.MemoryBarrier();
  return num;
}

由于实现的第二行存在完整的内存屏障,我相信VolatileRead实际上确保了上次写入address的值将被读取。根据维基百科的说法,“一个完整的围栏确保在栅栏之前的所有装载和存储操作都是在栅栏之后的任何负载和存储之前完成的。”

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

EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2014-08-01 20:22:28

我可能有点晚了,但我还是想插话。首先,我们需要就一些基本定义达成一致。

  • 获取-围栏:一个内存屏障,其中其他读写不允许移动在围栏之前。
  • 释放栅栏:一个内存屏障,在这个屏障之后,其他的读写被禁止移动。

我喜欢用箭头符号来帮助说明作用中的栅栏。↑箭头代表释放围栏,↓箭头表示获取围栏.将箭头头想象为按箭头的方向将内存访问推开。但是,这一点很重要,内存访问可以移动到尾部。阅读上面栅栏的定义,让自己相信箭头在视觉上代表了这些定义。

使用这个表示法,让我们分析JaredPar的回答中的示例,从Volatile.Read开始。但是,首先我要指出的是,Console.WriteLine可能会在我们不知情的情况下产生一个全栅栏的屏障。我们应该假装一下,这样做并不能使这些例子更容易理解。事实上,我将完全忽略这一呼吁,因为就我们正在努力实现的目标而言,这是不必要的。

代码语言:javascript
复制
// Example using Volatile.Read
x = 13;
var local = y; // Volatile.Read
↓              // acquire-fence
z = 13;

因此,使用箭头符号,我们可以更容易地看到,对z的写入不能在读取y之前向上移动。y的读取也不能向下移动,在写入z之后也不能移动,因为这实际上与相反的情况相同。换句话说,它锁定了yz的相对顺序。但是,由于没有箭头阻止移动,所以可以交换y的读取和对x的写入。同样,对x的写可以移动到箭头的尾部,甚至超过写到z。该规范在技术上允许使用that..theoretically。这意味着我们有以下有效的订单。

代码语言:javascript
复制
Volatile.Read
---------------------------------------
write x    |    read y     |    read y
read y     |    write x    |    write z
write z    |    write z    |    write x

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

代码语言:javascript
复制
// 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的写入。

代码语言:javascript
复制
Thread.VolatileRead
-----------------------
write x    |    read y
read y     |    write x
write z    |    write z

当然,有人声称微软的CLI ( .NET框架)和x86硬件的实现已经保证了所有写操作的发布围栏语义。因此,在这种情况下,这两个呼叫之间可能没有任何区别。和Mono一起在手臂处理器上?那样的话情况可能就不一样了。

现在让我们继续讨论你们的问题。

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

No..这不对!不稳定的阅读与“新鲜阅读”是不一样的。为什么?这是因为内存屏障被放置在读指令之后。这意味着实际读取仍然可以自由地向上或向后移动。另一个线程可以写入地址,但当前线程可能已经将读取移动到了另一个线程提交之前的某个时间点。

因此,这就引出了一个问题:“为什么人们会费心使用易失性的读物,如果它似乎保证这么少呢?”答案是,它绝对保证下一次阅读将比上一次阅读更新。这就是它的价值!这就是为什么很多无锁代码在循环中旋转,直到逻辑能够确定操作是否成功完成为止。换句话说,无锁代码利用了一个概念,即后面在多个读取序列中读取的代码将返回一个较新的值,但代码不应假定任何读取都必然表示最新的值。

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

但是,在考虑了上面讨论的“新鲜”读物的含义之后,你仍然想要一个“新鲜”的读物,然后你需要在阅读之前设置一个获取围栏。请注意,这显然与易失性读取不同,但它最好与开发人员对“新鲜”含义的直觉相匹配。然而,“新鲜”一词在本案中并不是绝对的。相反,相对于障碍来说,读起来是“新鲜的”。也就是说,它不可能比执行屏障的时间点更早。但是,如前所述,该值在使用或基于其做出决策时可能不代表最新的值。记住这一点。

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

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

票数 40
EN

Stack Overflow用户

发布于 2014-03-21 21:06:23

Volatile.Read在本质上保证读取和写入操作不能在读取之前移动。它对防止在读取之前发生的写入操作没有任何意义。例如

代码语言:javascript
复制
// assume x, y and z are declared 
x = 13;
Console.WriteLine(Volatile.Read(ref y));
z = 13;

不能保证对x的写入发生在y读取之前。但是,对z的写入保证在y读取之后发生。

代码语言:javascript
复制
// assume x, y and z are declared 
x = 13;
Console.WriteLine(Thread.VolatileRead(ref y));
z = 13;

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

  • 写x
  • 读y
  • 写z

全栅栏防止读写向任何一个方向移动。

票数 17
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/22569274

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档