任何人都可以在C#中对volatile关键字做出很好的解释吗?它解决了哪些问题,哪些问题没有解决?在哪些情况下它会省去锁定的使用?
答案 0 :(得分:260)
我不认为有更好的人回答这个问题而不是Eric Lippert(强调原文):
在C#中,“volatile”不仅意味着“确保编译器和 抖动不执行任何代码重新排序或注册缓存 对这个变量的优化“。它还意味着”告诉处理器 做任何他们需要做的事情,以确保我正在阅读 最新的价值,即使这意味着停止其他处理器和制造 他们将主内存与其缓存同步“。
实际上,最后一点是谎言。 volatile读取的真正语义 写作比我在这里概述的要复杂得多;在 事实它们实际上并不能保证每个处理器都能阻止它 正在进行并更新主存储器的缓存。相反,它们提供了 关于读取和读取之前和之后内存访问方式的较弱保证 可以观察到写入相对于彼此的排序。 某些操作,例如创建新线程,输入锁定或 使用Interlocked系列方法之一引入更强 关于观察订购的保证。如果你想要更多细节, 阅读C#4.0规范的第3.10和10.5.3节。
坦率地说,我不鼓励你做一个不稳定的领域。挥发物 字段表明你正在做一些彻头彻尾的疯狂:你是 尝试在两个不同的线程上读取和写入相同的值 没有锁定到位。锁保证内存读取或 在锁内部修改被观察到是一致的,锁保证 一次只有一个线程访问给定的内存块,等等 上。锁定速度太慢的情况非常多 小,以及你将错误的代码的概率 因为你不明白确切的内存模型是非常大的。一世 除了最琐碎的代码之外,不要尝试编写任何低锁代码 联锁作业的用法。我将“volatile”的用法留给了 真正的专家。
如需进一步阅读,请参阅:
答案 1 :(得分:55)
如果您想了解一下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;
}
使用标准优化(发布)编译器设置,编译器创建以下汇编程序(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
查看输出,编译器决定使用ecx寄存器来存储j变量的值。对于非易失性循环(第一个),编译器已将i分配给eax寄存器。非常坦率的。虽然有几个有趣的位 - lea ebx,[ebx]指令实际上是一个多字节nop指令,因此循环跳转到16字节对齐的内存地址。另一种是使用edx来增加循环计数器而不是使用inc eax指令。与inc reg指令相比,add reg,reg指令在少数IA32内核上具有更低的延迟,但从不具有更高的延迟。
现在循环使用volatile循环计数器。计数器存储在[esp]中,volatile关键字告诉编译器应始终从内存中读取/写入值,并且永远不会将其分配给寄存器。在更新计数器值时,编译器甚至不会执行加载/增量/存储作为三个不同的步骤(加载eax,inc eax,save eax),而是直接在单个指令中修改内存(添加内存) ,REG)。创建代码的方式可确保循环计数器的值始终在单个CPU内核的上下文中保持最新。对数据的操作不会导致损坏或数据丢失(因此不使用load / inc / store,因为值可能会在inc期间发生变化,从而在商店中丢失)。由于中断只能在当前指令完成后进行处理,因此即使存在未对齐的内存,数据也不会被破坏。
一旦将第二个CPU引入系统,volatile关键字将无法防止另一个CPU同时更新的数据。在上面的示例中,您需要将数据取消对齐以获得潜在的损坏。如果无法以原子方式处理数据,则volatile关键字不会阻止潜在的损坏,例如,如果循环计数器的类型为long long(64位),那么它将需要两个32位操作来更新值,在中间哪一个中断可以发生并改变数据。
因此,volatile关键字仅适用于小于或等于本机寄存器大小的对齐数据,因此操作始终是原子的。
volatile关键字被设想用于IO操作,其中IO将不断变化但具有恒定地址,例如存储器映射的UART设备,并且编译器不应该继续重用从地址读取的第一个值
如果您正在处理大数据或拥有多个CPU,那么您将需要一个更高级别(OS)锁定系统来正确处理数据访问。
答案 2 :(得分:38)
如果您使用的是.NET 1.1,则在执行双重检查锁定时需要使用volatile关键字。为什么?因为在.NET 2.0之前,以下场景可能导致第二个线程访问非空但尚未完全构造的对象:
在.NET 2.0之前,可以在构造函数完成运行之前为this.foo分配新的Foo实例。在这种情况下,第二个线程可以进入(在线程1调用Foo的构造函数期间)并体验以下内容:
在.NET 2.0之前,您可以将this.foo声明为易失性来解决此问题。从.NET 2.0开始,您不再需要使用volatile关键字来完成双重检查锁定。
维基百科实际上有一篇关于Double Checked Locking的好文章,并简要介绍了这个主题: http://en.wikipedia.org/wiki/Double-checked_locking
答案 3 :(得分:21)
来自MSDN: volatile修饰符通常用于多个线程访问的字段,而不使用lock语句来序列化访问。使用volatile修饰符可确保一个线程检索另一个线程写入的最新值。
答案 4 :(得分:20)
有时,编译器会优化字段并使用寄存器来存储它。如果线程1写入字段并且另一个线程访问它,由于更新存储在寄存器(而不是存储器)中,第二个线程将获得过时的数据。
您可以将volatile关键字视为编译器“我希望您将此值存储在内存中”。这可以保证第二个线程检索最新值。
答案 5 :(得分:13)
CLR喜欢优化指令,因此当您访问代码中的字段时,它可能无法始终访问字段的当前值(可能来自堆栈等)。将字段标记为volatile
可确保指令访问字段的当前值。当程序中的并发线程或操作系统中运行的其他代码可以修改(在非锁定方案中)时,这非常有用。
你显然失去了一些优化,但它确实使代码更简单。
答案 6 :(得分:1)
因此,总而言之,对该问题的正确答案是: 如果您的代码在2.0运行时或更高版本中运行,则几乎不需要volatile关键字,如果不必要地使用volatile关键字,则弊大于利。即永远不要使用它。但是在运行时的早期版本中,需要对静态字段进行适当的双重检查锁定。具体来说,其类具有静态类初始化代码的静态字段。
答案 7 :(得分:1)
我发现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
我会留在这里供参考
答案 8 :(得分:1)
只需查看volatile keyword的官方页面,您就可以看到一个典型用法示例。
public class Worker
{
public void DoWork()
{
bool work = false;
while (!_shouldStop)
{
work = !work; // simulate some work
}
Console.WriteLine("Worker thread: terminating gracefully.");
}
public void RequestStop()
{
_shouldStop = true;
}
private volatile bool _shouldStop;
}
在_shouldStop声明中添加volatile修饰符后,您将始终获得相同的结果。但是,如果_shouldStop成员上没有该修饰符,则该行为是无法预测的。
所以这绝对不是彻头彻尾的疯狂。
存在Cache coherence,它负责CPU缓存的一致性。
如果CPU使用strong memory model(作为x86)
因此,在x86上不需要对易失性字段进行读写操作:在x86上进行普通读写(例如,使用MOV指令)就足够了。
C#5.0规范中的示例(第10.5.3章)
using System;
using System.Threading;
class Test
{
public static int result;
public static volatile bool finished;
static void Thread2() {
result = 143;
finished = true;
}
static void Main() {
finished = false;
new Thread(new ThreadStart(Thread2)).Start();
for (;;) {
if (finished) {
Console.WriteLine("result = {0}", result);
return;
}
}
}
}
产生输出:result = 143
如果尚未将finished字段声明为volatile,那么在存储完成后,允许存储结果对主线程可见,从而使主线程从该字段读取值0结果。
易受攻击的行为取决于平台,因此您应始终视情况考虑使用volatile
,以确保它满足您的需求。
即使volatile
也无法阻止(各种)重新排序(C# - The C# Memory Model in Theory and Practice, Part 2)
即使对A的写入是易失的,并且对A_Won的读取也是易失的,围栏都是单向的,实际上允许这种重新排序。
因此,我相信,如果您想知道何时使用volatile
(与lock
和Interlocked
相比),则应该熟悉由它们发出的内存屏障(全部,一半),请检查栅栏在特定平台(框架,操作系统,CPU)上的行为如何,那么您将为自己的利益得到宝贵的答案。
答案 9 :(得分:0)
编译器有时会更改代码中语句的顺序以优化它。通常这在单线程环境中不是问题,但在多线程环境中可能是一个问题。请参阅以下示例:
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);
}
});
如果你运行t1和t2,你会得到没有输出或“Value:10”的结果。可能是编译器在t1函数内部切换行。如果t2然后执行,则可能是_flag的值为5,但_value为0.因此预期的逻辑可能被破坏。
要解决此问题,您可以使用可以应用于该字段的易失性关键字。此语句禁用编译器优化,因此您可以在代码中强制执行正确的顺序。
private static volatile int _flag = 0;
只有在你确实需要时才应该使用 volatile ,因为它会禁用某些编译器优化,这会损害性能。它也不受所有.NET语言的支持(Visual Basic不支持它),因此它阻碍了语言的互操作性。
答案 10 :(得分:-3)
多个线程可以访问变量。 最新的更新将在变量
上