易失性 vs 可读/写的易失性?

30
我找不到任何VolatileRead/write(尝试...)的示例,但仍然有疑问:何时应该使用volatile与VolatileRead?据我所知,volatile的整个目的是创建一半的屏障,因此:
1. 对于读操作,在当前操作之后读取/写入(在其他线程上),在屏障之前不会通过。因此,我们读取最新的值。
问题1:
那么为什么我需要volatileRead?看起来volatile已经做了这项工作。
另外,在C#中,所有写操作都是volatile的(不像Java),无论您是写入volatile字段还是非volatile字段 - 因此我问:为什么我需要volatileWrite?
问题2:
这是VolatileRead的实现:
[MethodImpl(MethodImplOptions.NoInlining)]
public static int VolatileRead(ref int address)
{
    int num = address;
    MemoryBarrier();
    return num;
}

为什么要有这行代码:int num = address;?明明已经有了参数address,它显然保存了值。


这是一个关于volatile read的类似链接:https://dev59.com/FEXRa4cB1Zd3GeqPv_SD - Sakthivel
@codebrain,我已经看过了,但是没有一个答案回答了我的问题。(我应该在什么情况下使用每个关键字,为什么还要存在它们,如果volatile已经实现了它们的功能。此外,没有一个代码示例说明如何使用它们)。尝试使用ctrl+f搜索“volatileRead” - 除了标题之外,你将找不到任何内容。 - Royi Namir
4
volatile关键字只能用于字段上。如果要在多个线程之间共享非字段内存位置(比如闭包中的变量)并获得相同的语义,可以使用VolatileReadVolatileWrite。请注意,这不会改变原始意思。 - anton.burger
@svick 你说得对。只是它们两个非常接近,所以我想我可以问问他们...但你说得对。 - Royi Namir
我认为这突显了对volatile用途的误解。尽管我自己也不是这个话题的专家,但屏障可以防止在同一个线程上对操作的重新排序,而我所学到的一切都表明它们不能确保您看到来自其他线程的最新更新,而是你会看到由同一个线程进行的一整组相关更新,或根本没有看到。 - anton.burger
显示剩余5条评论
4个回答

37
不应使用Thread.VolatileRead/Write()。这是.NET 1.1中的设计错误,它使用完整的内存屏障。这在.NET 2.0中得到了纠正,但是它们无法再修复这些方法,因此必须添加一个新的方法,由System.Threading.Volatile类提供。这是一个JIT知道的类,它在jit时将方法替换为适用于特定处理器类型的版本。
参考源代码中Volatile类的注释(编辑过)详细说明了此事。
// Methods for accessing memory with volatile semantics.  These are preferred over 
// Thread.VolatileRead and Thread.VolatileWrite, as these are implemented more
// efficiently.
//
// (We cannot change the implementations of Thread.VolatileRead/VolatileWrite 
// without breaking code that relies on their overly-strong ordering guarantees.)
//
// The actual implementations of these methods are typically supplied by the VM at 
// JIT-time, because C# does not allow us to express a volatile read/write from/to 
// a byref arg. See getILIntrinsicImplementationForVolatile() in jitinterface.cpp.

是的,你可能会很难找到它使用的示例。参考源是一个极好的指南,有着经过精心撰写、测试和打磨的C#代码,用于处理线程。它使用VolatileRead/Write的次数为

坦白说,.NET内存模型非常混乱,CLR mm和C# mm之间存在冲突的假设,并且最近为ARM核添加了新规则。volatile关键字的奇怪语义在不同体系结构上意味着不同的含义,这是一些证据。尽管对于具有弱内存模型的处理器,您通常可以假定C#语言规范所说的是准确的。

请注意,Joe Duffy已经放弃了所有希望,并且彻底反对所有使用它的行为。通常情况下,假设您可以比语言和框架提供的基元更好地完成任务是非常不明智的。 Volatile类的备注部分说明了这一点:

在正常情况下,C#的lock语句、Visual Basic的SyncLock语句以及Monitor类提供了最简单且最少出错的同步访问数据的方式。而Lazy类则提供了一种简单的方法来编写懒加载代码,而不需要直接使用双重检查锁定。

1
当我说“找不到任何示例”时,我并不是在开玩笑...:-)...但确实非常奇怪。谷歌里竟然没有一个样例。您能告诉我从哪里下载所有的文档吗? - Royi Namir
不清楚你要求的“文档”是什么。参考源代码在此处提供:http://referencesource.microsoft.com/netframework.aspx - Hans Passant
1
在C#中,volatile关键字是否具有与Volatile.Read()和Volatile.Write()相同的获取/释放语义?在Mono 4.x中,行为是不同的。 - Petrakeas
1
"Volatile is evil"(http://joeduffyblog.com/2010/12/04/sayonara-volatile/)。 - Hans Passant
现在在 .Net Core 3.1 中,可以找到大量的易失性读写操作。 - Dennis19901

8
当您需要更精细地控制围栏应用于代码的方式时,可以使用staticThread.VolatileReadThread.VolatileWrite。声明变量为volatile意味着编译器不会缓存其值,并始终读取字段值,当执行写操作时,编译器会立即写入分配的值。 Thread.VolatileReadThread.VolatileWrite这两种方法使您能够在不声明变量为volatile的情况下拥有更精细的控制,因为您可以决定何时执行volatile读操作和何时进行volatile写操作,而无需绑定到声明variale volatile时具有的不缓存并立即写入的限制,因此用简单的话来说,您拥有更多的控制和自由...

VolatileRead() 读取内存地址的最新版本,VolatileWrite() 写入该地址,使地址可供所有线程使用。在变量上始终同时使用 VolatileRead()VolatileWrite() 具有与将其标记为 volatile 相同的效果。

请查看 这篇博客文章,通过示例解释它们之间的区别...

为什么要有 int num = address; 这一行?他们已经有明显持有值的 address 参数了。

这是一种防御性复制,以避免我们在方法内部时发生外部更改值的情况,整数值被复制到本地变量中,以避免意外的外部更改。

注释

由于在 Visual Basic 中不存在 volatile 关键字,因此您唯一的选择是始终使用 VolatileRead()VolatileWrite() 静态方法来实现与 c# 中 volatile 关键字相同的效果。


1
“fine grained control over the way fences” 是什么意思?请告诉我一个 volatile 无法完成但 volatile read/write 可以完成的任务,这会让我学到很多。作者还说:“为了更好地控制我们的代码中应用的栅栏”,然后他就停了下来,没有解释。(而且我也看过这篇文章... :-)) - Royi Namir
“立即写入”默认就是……不是吗?为什么还需要一个“volatile写入”方法呢? - Royi Namir
这是更多控制的问题,使用易失变量时,您不能决定仅执行未缓存的读取操作或立即写入操作,您必须无选择地同时执行两者...请查看更新的答案,应该会更加清晰。 - aleroot

4
为什么有一行 int num = address;?它们已经有明确保存值的参数 address。
address 不是 int 类型,而是 int*(一个地址)。代码对指针进行解引用并将其复制到本地变量中,以便屏障在解引用之后发生。

void Main() { int g=2; aaa(ref g); } public void aaa(ref int address) { Console.WriteLine ( address); //2 } - Royi Namir
返回 address 将会返回其值。(我没有问为什么引用,我问的是为什么代理赋值。) - Royi Namir
在没有代理赋值的情况下,你会如何编写内存屏障?该屏障必须在解引用之后返回之前。由于将变量放置在寄存器中会带来性能开销,因此需要考虑这一点。 - usr
抱歉,也许我有点生疏,但是 public static int VolatileRead(ref int address) { MemoryBarrier(); return address; } 有什么问题? - Royi Namir
4
你正在读取内存屏障后的 int。你明白我关于地址是指向 int 而不是 int 的指针的注释吗?请注意它是 ref int,而不是 int。 - usr

1
为了进一步阐述aleroot的答案,Volatile.Read和Volatile.Write与volatile修饰符相同,正如Royi Namir所说。但是你可以明智地使用它们。
例如,如果你使用volatile修饰符声明一个字段,那么对该字段的每次访问(无论是读操作还是写操作),都将从CPU寄存器中读取,这在大多数情况下是不必要的,并且如果该字段甚至具有多个读操作,则会导致不必要的性能损失。
想象一下这样的情况:你声明了一个私有单例变量并将其声明为volatile,在属性getter中返回它。一旦它被初始化,你就不需要从CPU寄存器中读取它的根,因此你可以使用Volatile.Read / Write直到它的实例被创建,一旦创建,所有的读操作都可以像正常字段一样进行,否则将会产生巨大的性能损失。
而你可以根据需要使用Volatile.Read或Volatile.Write。 最佳用法是声明不带volatile修饰符的字段,并在需要时使用Volatile.Read或Volatile.Write。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接