有人能很好地解释一下c#中的volatile关键字吗?它能解决哪些问题,不能解决哪些问题?在哪些情况下,它将节省我使用锁定?
当前回答
编译器有时会改变代码中语句的顺序来优化它。通常这在单线程环境中不是问题,但在多线程环境中可能是问题。请看下面的例子:
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。因此,预期的逻辑可能会被打破。
为了解决这个问题,你可以使用volatile关键字,你可以应用到字段。此语句禁用编译器优化,以便您可以强制代码中的正确顺序。
private static volatile int _flag = 0;
只有在真正需要时才应该使用volatile,因为它会禁用某些编译器优化,这会影响性能。它也不是所有。net语言都支持(Visual Basic不支持),因此它阻碍了语言互操作性。
其他回答
多个线程可以访问一个变量。 最新的更新将在变量上
我认为没有比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 doing and 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.
欲进一步阅读,请参阅:
理解低锁技术在多线程应用中的影响 再见不稳定
有时候,编译器会优化一个字段并使用寄存器来存储它。如果线程1写了字段,而另一个线程访问了它,因为更新存储在寄存器(而不是内存)中,第二个线程将得到陈旧的数据。
你可以把volatile关键字看作是对编译器说“我想让你把这个值存储在内存中”。这保证了第二个线程检索到最新的值。
如果你想稍微了解一下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
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.
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.
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.
因此,volatile关键字只适用于小于或等于本机寄存器大小的对齐数据,这样操作总是原子的。
volatile关键字被设想用于IO操作,其中IO将不断变化,但有一个恒定的地址,例如内存映射的UART设备,编译器不应该一直重用从地址中读取的第一个值。
如果要处理大数据或有多个cpu,则需要更高级别(OS)的锁定系统来正确处理数据访问。
编译器有时会改变代码中语句的顺序来优化它。通常这在单线程环境中不是问题,但在多线程环境中可能是问题。请看下面的例子:
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。因此,预期的逻辑可能会被打破。
为了解决这个问题,你可以使用volatile关键字,你可以应用到字段。此语句禁用编译器优化,以便您可以强制代码中的正确顺序。
private static volatile int _flag = 0;
只有在真正需要时才应该使用volatile,因为它会禁用某些编译器优化,这会影响性能。它也不是所有。net语言都支持(Visual Basic不支持),因此它阻碍了语言互操作性。
推荐文章
- CountDownLatch是如何在Java多线程中使用的?
- 如何强制LINQ Sum()返回0而源集合是空的
- 如果我同步了同一个类上的两个方法,它们能同时运行吗?
- 将值附加到查询字符串
- Selenium c# WebDriver:等待元素出现
- 我如何添加双引号的字符串,是在一个变量?
- 检查字符串是否包含字符串列表中的元素
- 最好的方法在asp.net强制https为整个网站?
- 等待未来的名单
- 将字符串转换为System.IO.Stream
- 如何从枚举中选择一个随机值?
- 驻留在App_Code中的类不可访问
- 在链式LINQ扩展方法调用中等价于'let'关键字的代码
- dynamic (c# 4)和var之间的区别是什么?
- Visual Studio: ContextSwitchDeadlock