Dor*_*oby 290 c# multithreading
任何人都可以在C#中对volatile关键字提供一个很好的解释吗?它解决了哪些问题,哪些问题没有解决?在哪些情况下它会节省我使用锁定?
Oha*_*der 266
我不认为有一个更好的人来回答这个问题,而不是Eric Lippert(强调原文):
在C#中,"volatile"不仅意味着"确保编译器和抖动不对此变量执行任何代码重新排序或寄存器缓存优化".它还意味着"告诉处理器做他们需要做的任何事情,以确保我正在读取最新的值,即使这意味着停止其他处理器并使它们与主存储器的缓存同步".
实际上,最后一点是谎言.易失性读写的真正语义比我在此概述的要复杂得多; 实际上,它们实际上并不能保证每个处理器都停止正在进行的操作并更新主存储器的缓存.相反,它们提供了关于如何观察读取和写入之前和之后的存储器访问相对于彼此进行排序的较弱保证.某些操作(如创建新线程,输入锁定或使用Interlocked系列方法)可以为观察排序提供更强有力的保证.如果您想了解更多详细信息,请阅读C#4.0规范的第3.10和10.5.3节.
坦率地说,我不鼓励你做一个不稳定的领域.易失性字段表明你正在做一些彻头彻尾的疯狂:你试图在两个不同的线程上读取和写入相同的值,而不是锁定到位.锁定保证锁内部读取或修改的内存一致,锁定保证一次只有一个线程访问给定的内存块,依此类推.锁定速度太慢的情况非常少,并且由于您不了解确切的内存模型而导致代码错误的可能性非常大.除了Interlocked操作最琐碎的用法之外,我不会尝试编写任何低锁代码.我将"挥发性"的用法留给了真正的专家.
进一步阅读请参阅:
Ski*_*izz 56
如果您想了解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;
}
Run Code Online (Sandbox Code Playgroud)
使用标准优化(发布)编译器设置,编译器创建以下汇编程序(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
Run Code Online (Sandbox Code Playgroud)
查看输出,编译器决定使用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)锁定系统来正确处理数据访问.
小智 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
Ben*_*oit 23
有时,编译器将优化字段并使用寄存器来存储它.如果线程1对字段进行写操作而另一个线程访问它,则由于更新存储在寄存器(而不是存储器)中,因此第二个线程将获得过时的数据.
您可以将volatile关键字视为编译器"我希望您将此值存储在内存中".这可以保证第二个线程检索最新值.
Jos*_*gle 13
CLR喜欢优化指令,因此当您访问代码中的字段时,它可能无法始终访问字段的当前值(可能来自堆栈等).将字段标记为volatile
确保指令访问字段的当前值.当程序中的并发线程或操作系统中运行的某些其他代码可以修改(在非锁定方案中)时,这非常有用.
你显然失去了一些优化,但它确实使代码更简单.
我发现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
我只是把它留在这里以供参考
Simply looking into the official page for volatile keyword you can see an example of typical usage.
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;
}
Run Code Online (Sandbox Code Playgroud)
With the volatile modifier added to the declaration of _shouldStop in place, you'll always get the same results. However, without that modifier on the _shouldStop member, the behavior is unpredictable.
So this is definitely not something downright crazy.
There exists Cache coherence that is responsible for CPU caches consistency.
Also if CPU employs strong memory model (as x86)
As a result, reads and writes of volatile fields require no special instructions on the x86: Ordinary reads and writes (for example, using the MOV instruction) are sufficient.
Example from C# 5.0 specification (chapter 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;
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
produces the output: result = 143
If the field finished had not been declared volatile, then it would be permissible for the store to result to be visible to the main thread after the store to finished, and hence for the main thread to read the value 0 from the field result.
Volatile behavior is platform dependent so you should always consider using volatile
when needed by case to be sure it satisfies your needs.
Even volatile
could not prevent (all kind of) reordering (C# - The C# Memory Model in Theory and Practice, Part 2)
Even though the write to A is volatile and the read from A_Won is also volatile, the fences are both one-directional, and in fact allow this reordering.
So I believe if you want to know when to use volatile
(vs lock
vs Interlocked
) you should get familiar with memory fences (full, half) and needs of a synchronization. Then you get your precious answer yourself for your good.
归档时间: |
|
查看次数: |
80138 次 |
最近记录: |