什么时候应该在 C# 中使用 volatile 关键字?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/72275/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
When should the volatile keyword be used in C#?
提问by Doron Yaacoby
Can anyone provide a good explanation of the volatile keyword in C#? Which problems does it solve and which it doesn't? In which cases will it save me the use of locking?
任何人都可以对 C# 中的 volatile 关键字提供一个很好的解释吗?它解决了哪些问题,没有解决哪些问题?在哪些情况下它可以节省我使用锁定的时间?
采纳答案by Ohad Schneider
I don't think there's a better person to answer this than Eric Lippert(emphasis in the original):
我认为没有比Eric Lippert更好的人来回答这个问题(强调原文):
In C#, "volatile" means not only "make sure that the compiler and the jitter do not perform any code reordering or register caching optimizations on this variable". It also means "tell the processors to do whatever it is they need to do to ensure that I am reading the latest value, even if that means halting other processors and making them synchronize main memory with their caches".
Actually, that last bit is a lie. The true semantics of volatile reads and writes are considerably more complex than I've outlined here; in fact they do not actually guarantee that every processor stops what it is doingand updates caches to/from main memory. Rather, they provide weaker guarantees about how memory accesses before and after reads and writes may be observed to be ordered with respect to each other. Certain operations such as creating a new thread, entering a lock, or using one of the Interlocked family of methods introduce stronger guarantees about observation of ordering. If you want more details, read sections 3.10 and 10.5.3 of the C# 4.0 specification.
Frankly, I discourage you from ever making a volatile field. Volatile fields are a sign that you are doing something downright crazy: you're attempting to read and write the same value on two different threads without putting a lock in place. Locks guarantee that memory read or modified inside the lock is observed to be consistent, locks guarantee that only one thread accesses a given chunk of memory at a time, and so on. The number of situations in which a lock is too slow is very small, and the probability that you are going to get the code wrong because you don't understand the exact memory model is very large. I don't attempt to write any low-lock code except for the most trivial usages of Interlocked operations. I leave the usage of "volatile" to real experts.
在 C# 中,“volatile”不仅意味着“确保编译器和抖动不会对该变量执行任何代码重新排序或寄存器缓存优化”。它还意味着“告诉处理器做他们需要做的任何事情以确保我正在读取最新值,即使这意味着停止其他处理器并使它们同步主内存与它们的缓存”。
事实上,最后一点是谎言。易失性读取和写入的真正语义比我在这里概述的要复杂得多;事实上,它们实际上并不能保证每个处理器都停止它正在做的事情并将缓存更新到/从主内存。相反,它们提供了较弱的保证,即可以观察到读取和写入前后的内存访问如何相对于彼此进行排序。某些操作(例如创建新线程、输入锁或使用 Interlocked 系列方法之一)引入了对顺序观察的更强保证。如果您需要更多详细信息,请阅读 C# 4.0 规范的 3.10 和 10.5.3 部分。
坦率地说,我不鼓励你创建一个不稳定的字段。可变字段表明您正在做一些非常疯狂的事情:您试图在没有锁定的情况下在两个不同的线程上读取和写入相同的值。锁保证在锁内读取或修改的内存被观察到是一致的,锁保证一次只有一个线程访问给定的内存块,依此类推。锁太慢的情况非常少,因为不了解确切的内存模型而导致代码出错的概率非常大。除了 Interlocked 操作的最琐碎用法之外,我不会尝试编写任何低锁代码。我将“易失性”的用法留给真正的专家。
For further reading see:
如需进一步阅读,请参见:
回答by Joseph Daigle
The CLR likes to optimize instructions, so when you access a field in code it might not always access the current value of the field (it might be from the stack, etc). Marking a field as volatile
ensures that the current value of the field is accessed by the instruction. This is useful when the value can be modified (in a non-locking scenario) by a concurrent thread in your program or some other code running in the operating system.
CLR 喜欢优化指令,因此当您访问代码中的字段时,它可能并不总是访问该字段的当前值(它可能来自堆栈等)。将字段标记为volatile
可确保指令访问该字段的当前值。当程序中的并发线程或操作系统中运行的其他代码可以修改值(在非锁定场景中)时,这很有用。
You obviously lose some optimization, but it does keep the code more simple.
你显然失去了一些优化,但它确实让代码更简单。
回答by Dr. Bob
From MSDN: The volatile modifier is usually used for a field that is accessed by multiple threads without using the lock statement to serialize access. Using the volatile modifier ensures that one thread retrieves the most up-to-date value written by another thread.
来自MSDN: volatile 修饰符通常用于由多个线程访问的字段,而不使用 lock 语句来序列化访问。使用 volatile 修饰符可确保一个线程检索另一个线程写入的最新值。
回答by Benoit
Sometimes, the compiler will optimize a field and use a register to store it. If thread 1 does a write to the field and another thread accesses it, since the update was stored in a register (and not memory), the 2nd thread would get stale data.
有时,编译器会优化一个字段并使用寄存器来存储它。如果线程 1 对该字段进行写操作,而另一个线程访问它,因为更新存储在寄存器(而不是内存)中,第二个线程将获得陈旧数据。
You can think of the volatile keyword as saying to the compiler "I want you to store this value in memory". This guarantees that the 2nd thread retrieves the latest value.
您可以将 volatile 关键字视为对编译器说“我希望您将此值存储在内存中”。这保证了第二个线程检索最新值。
回答by Skizz
If you want to get slightly more technical about what the volatile keyword does, consider the following program (I'm using DevStudio 2005):
如果您想稍微了解一下 volatile 关键字的功能,请考虑以下程序(我使用的是 DevStudio 2005):
#include <iostream>
void main()
{
int j = 0;
for (int i = 0 ; i < 100 ; ++i)
{
j += i;
}
for (volatile int i = 0 ; i < 100 ; ++i)
{
j += i;
}
std::cout << j;
}
Using the standard optimised (release) compiler settings, the compiler creates the following assembler (IA32):
使用标准优化(发布)编译器设置,编译器创建以下汇编器 (IA32):
void main()
{
00401000 push ecx
int j = 0;
00401001 xor ecx,ecx
for (int i = 0 ; i < 100 ; ++i)
00401003 xor eax,eax
00401005 mov edx,1
0040100A lea ebx,[ebx]
{
j += i;
00401010 add ecx,eax
00401012 add eax,edx
00401014 cmp eax,64h
00401017 jl main+10h (401010h)
}
for (volatile int i = 0 ; i < 100 ; ++i)
00401019 mov dword ptr [esp],0
00401020 mov eax,dword ptr [esp]
00401023 cmp eax,64h
00401026 jge main+3Eh (40103Eh)
00401028 jmp main+30h (401030h)
0040102A lea ebx,[ebx]
{
j += i;
00401030 add ecx,dword ptr [esp]
00401033 add dword ptr [esp],edx
00401036 mov eax,dword ptr [esp]
00401039 cmp eax,64h
0040103C jl main+30h (401030h)
}
std::cout << j;
0040103E push ecx
0040103F mov ecx,dword ptr [__imp_std::cout (40203Ch)]
00401045 call dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (402038h)]
}
0040104B xor eax,eax
0040104D pop ecx
0040104E ret
Looking at the output, the compiler has decided to use the ecx register to store the value of the j variable. For the non-volatile loop (the first) the compiler has assigned i to the eax register. Fairly straightforward. There are a couple of interesting bits though - the lea ebx,[ebx] instruction is effectively a multibyte nop instruction so that the loop jumps to a 16 byte aligned memory address. The other is the use of edx to increment the loop counter instead of using an inc eax instruction. The add reg,reg instruction has lower latency on a few IA32 cores compared to the inc reg instruction, but never has higher latency.
查看输出,编译器决定使用 ecx 寄存器来存储 j 变量的值。对于非易失性循环(第一个),编译器已将 i 分配给 eax 寄存器。非常坦率的。不过有几个有趣的地方—— lea ebx,[ebx] 指令实际上是一个多字节 nop 指令,因此循环跳转到一个 16 字节对齐的内存地址。另一种是使用 edx 来增加循环计数器,而不是使用 inc eax 指令。与 inc reg 指令相比,add reg,reg 指令在几个 IA32 内核上具有更低的延迟,但从未有更高的延迟。
Now for the loop with the volatile loop counter. The counter is stored at [esp] and the volatile keyword tells the compiler the value should always be read from/written to memory and never assigned to a register. The compiler even goes so far as to not do a load/increment/store as three distinct steps (load eax, inc eax, save eax) when updating the counter value, instead the memory is directly modified in a single instruction (an add mem,reg). The way the code has been created ensures the value of the loop counter is always up-to-date within the context of a single CPU core. No operation on the data can result in corruption or data loss (hence not using the load/inc/store since the value can change during the inc thus being lost on the store). Since interrupts can only be serviced once the current instruction has completed, the data can never be corrupted, even with unaligned memory.
现在是带有 volatile 循环计数器的循环。计数器存储在 [esp] 并且 volatile 关键字告诉编译器该值应该始终从内存读取/写入内存,并且永远不要分配给寄存器。编译器甚至在更新计数器值时不将加载/增量/存储作为三个不同的步骤(加载 eax、inc eax、保存 eax),而是直接在单个指令中修改内存(添加内存,注册)。创建代码的方式确保循环计数器的值在单个 CPU 内核的上下文中始终是最新的。对数据的任何操作都不会导致损坏或数据丢失(因此不使用 load/inc/store,因为值可能会在 inc 期间更改,从而在 store 中丢失)。由于中断只能在当前指令完成后服务,
Once you introduce a second CPU to the system, the volatile keyword won't guard against the data being updated by another CPU at the same time. In the above example, you would need the data to be unaligned to get a potential corruption. The volatile keyword won't prevent potential corruption if the data cannot be handled atomically, for example, if the loop counter was of type long long (64 bits) then it would require two 32 bit operations to update the value, in the middle of which an interrupt can occur and change the data.
一旦将第二个 CPU 引入系统, volatile 关键字将不会防止数据同时被另一个 CPU 更新。在上面的示例中,您需要未对齐数据才能获得潜在的损坏。如果无法以原子方式处理数据,则 volatile 关键字将无法防止潜在的损坏,例如,如果循环计数器的类型为 long long(64 位),则需要两次 32 位操作来更新值,在中间可以发生中断并更改数据。
So, the volatile keyword is only good for aligned data which is less than or equal to the size of the native registers such that operations are always atomic.
因此, volatile 关键字仅适用于小于或等于本机寄存器大小的对齐数据,以便操作始终是原子的。
The volatile keyword was conceived to be used with IO operations where the IO would be constantly changing but had a constant address, such as a memory mapped UART device, and the compiler shouldn't keep reusing the first value read from the address.
volatile 关键字被设想用于 IO 操作,其中 IO 将不断变化但具有恒定地址,例如内存映射的 UART 设备,并且编译器不应继续重用从地址读取的第一个值。
If you're handling large data or have multiple CPUs then you'll need a higher level (OS) locking system to handle the data access properly.
如果您正在处理大数据或有多个 CPU,那么您将需要更高级别 (OS) 锁定系统来正确处理数据访问。
回答by user2943601
multiple threads can access a variable. The latest update will be on the variable
多个线程可以访问一个变量。最新更新将在变量上
回答by AndrewTek
If you are using .NET 1.1, the volatile keyword is needed when doing double checked locking. Why? Because prior to .NET 2.0, the following scenario could cause a second thread to access an non-null, yet not fully constructed object:
如果您使用 .NET 1.1,则在执行双重检查锁定时需要 volatile 关键字。为什么?因为在 .NET 2.0 之前,以下场景可能会导致第二个线程访问非空但未完全构造的对象:
- Thread 1 asks if a variable is null. //if(this.foo == null)
- Thread 1 determines the variable is null, so enters a lock. //lock(this.bar)
- Thread 1 asks AGAIN if the variable is null. //if(this.foo == null)
- Thread 1 still determines the variable is null, so it calls a constructor and assigns the value to the variable. //this.foo = new Foo();
- 线程 1 询问变量是否为空。//if(this.foo == null)
- 线程 1 确定变量为空,因此进入锁。//锁定(this.bar)
- 线程 1 再次询问变量是否为空。//if(this.foo == null)
- 线程 1 仍然确定变量为空,因此它调用构造函数并将值分配给变量。//this.foo = new Foo();
Prior to .NET 2.0, this.foo could be assigned the new instance of Foo, before the constructor was finished running. In this case, a second thread could come in (during thread 1's call to Foo's constructor) and experience the following:
在 .NET 2.0 之前, this.foo 可以在构造函数完成运行之前分配给 Foo 的新实例。在这种情况下,第二个线程可能会进入(在线程 1 调用 Foo 的构造函数期间)并经历以下情况:
- Thread 2 asks if variable is null. //if(this.foo == null)
- Thread 2 determines the variable is NOT null, so tries to use it. //this.foo.MakeFoo()
- 线程 2 询问变量是否为空。//if(this.foo == null)
- 线程 2 确定该变量不为空,因此尝试使用它。//this.foo.MakeFoo()
Prior to .NET 2.0, you could declare this.foo as being volatile to get around this problem. Since .NET 2.0, you no longer need to use the volatile keyword to accomplish double checked locking.
在 .NET 2.0 之前,您可以将 this.foo 声明为 volatile 以解决此问题。从 .NET 2.0 开始,您不再需要使用 volatile 关键字来完成双重检查锁定。
Wikipedia actually has a good article on Double Checked Locking, and briefly touches on this topic: http://en.wikipedia.org/wiki/Double-checked_locking
维基百科实际上有一篇关于双重检查锁定的好文章,并简要介绍了这个主题:http: //en.wikipedia.org/wiki/Double-checked_locking
回答by Aliaksei Maniuk
The compiler sometimes changes the order of statements in code to optimize it. Normally this is not a problem in single-threaded environment, but it might be an issue in multi-threaded environment. See following example:
编译器有时会更改代码中语句的顺序以对其进行优化。通常这在单线程环境中不是问题,但在多线程环境中可能会出现问题。请参阅以下示例:
private static int _flag = 0;
private static int _value = 0;
var t1 = Task.Run(() =>
{
_value = 10; /* compiler could switch these lines */
_flag = 5;
});
var t2 = Task.Run(() =>
{
if (_flag == 5)
{
Console.WriteLine("Value: {0}", _value);
}
});
If you run t1 and t2, you would expect no output or "Value: 10" as the result. It could be that the compiler switches line inside t1 function. If t2 then executes, it could be that _flag has value of 5, but _value has 0. So expected logic could be broken.
如果您运行 t1 和 t2,您会期望没有输出或“值:10”作为结果。可能是编译器在 t1 函数内切换了行。如果 t2 然后执行,则 _flag 的值可能为 5,但 _value 的值为 0。因此可能会破坏预期的逻辑。
To fix this you can use volatilekeyword that you can apply to the field. This statement disables the compiler optimizations so you can force the correct order in you code.
要解决此问题,您可以使用可应用于该字段的volatile关键字。此语句禁用编译器优化,因此您可以在代码中强制使用正确的顺序。
private static volatile int _flag = 0;
You should use volatileonly if you really need it, because it disables certain compiler optimizations, it will hurt performance. It's also not supported by all .NET languages (Visual Basic doesn't support it), so it hinders language interoperability.
只有在确实需要时才应该使用volatile,因为它禁用了某些编译器优化,会损害性能。并非所有 .NET 语言都支持它(Visual Basic 不支持它),因此它阻碍了语言互操作性。
回答by Paul Easter
So to sum up all this, the correct answer to the question is: If your code is running in the 2.0 runtime or later, the volatile keyword is almost never needed and does more harm than good if used unnecessarily. I.E. Don't ever use it. BUT in earlier versions of the runtime, it IS needed for proper double check locking on static fields. Specifically static fields whose class has static class initialization code.
综上所述,问题的正确答案是:如果您的代码在 2.0 运行时或更高版本中运行,则几乎不需要 volatile 关键字,如果不必要地使用,则弊大于利。IE 永远不要使用它。但是在运行时的早期版本中,需要对静态字段进行正确的双重检查锁定。特别是其类具有静态类初始化代码的静态字段。
回答by Christina Katsakoula
I found this article by Joydip Kanjilalvery helpful!
我发现Joydip Kanjilal 的这篇文章很有帮助!
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
When you mark an object or a variable as volatile, it becomes a candidate for volatile reads and writes. It should be noted that in C# all memory writes are volatile irrespective of whether you are writing data to a volatile or a non-volatile object. However, the ambiguity happens when you are reading data. When you are reading data that is non-volatile, the executing thread may or may not always get the latest value. If the object is volatile, the thread always gets the most up-to-date value
I'll just leave it here for reference
我只是把它留在这里以供参考