【C#】volatile 和内存屏障
warning:
这篇文章距离上次修改已过487天,其中的内容可能已经有所变动。
半内存屏障
例如有这样的程序,Init()
和Print()
在不同的线程上执行。
int data = 0;
bool inited = false; // 指示data字段是否已经设置的标志
void Init(){
data = 42;
inited = true; // 设置标志,指示data字段已经设置好了
}
void Print(){
// 如果有inited的标志,打印data字段的值
// 否则打印 Not initialized!
if(inited){
Console.WriteLine(data);
}else{
Console.WriteLine("Not initialized!");
}
}
- 一个线程执行
Init()
,在设置好data
字段的值后,设置inited
字段用来指示data
的值已经设置好了。 - 另一个线程执行
Print()
,在读取到inited
字段为true时,data
字段的值应该已经设置好了。
但是由于编译器、JIT和CPU都会重排内存操作指令来优化效率,所以可能会发生下面两种异常情况:
Init()
中,写入inited
在写入data
之前执行,即data
字段还没写入值,却已经设置了inited
的标志。Print()
中,读取data
在读取inited
之前执行,就算读取到了inited
为true,data
的值也可能是未被设置的值(例如,data
读取之后、inited
读取之前,inited
刚刚被设置为true)。
被volatile
关键字修饰的字段在被写入或被读取时,会加上内存屏障:
- volatile write:具有release(发布,已完成写入)内存屏障,之前(上面)的语句不能被重排到之后(下面)。
- volatile read:具有acquire(获取,准备读取)内存屏障,之后(下面)的语句不能被重排到之前(上面)。
现在:
int data = 0;
volatile bool inited = false; // 添加了volatile修饰
void Init(){
data = 42;
// 写入inited是volatile write,上面的语句不能被重排到下面,即data写入不会在inited写入之后执行
inited = true;
}
void Print(){
// 读取inited是volatile read,下面的语句不能被重排到上面,即data读取不会在inited读取之前执行。
if(inited){
Console.WriteLine(data);
}else{
Console.WriteLine("Not initialized!");
}
}
因为内存屏障限制了指令重排,所以在写入inited
的时候,data
已经被写入好了;在读取inited
的时候,data
不会被提前读取。
全内存屏障
Thread.MemoryBarrier()
会产生一个完全的内存屏障,即屏障之前的指令不能重排到屏障之后,屏障之后的指令也不能重排到屏障之前。
Interlocked
的任何操作也会产生全内存屏障,之前和之后的任何指令都不会跨越Interlocked
操作。
总结
这篇文章只是用来介绍内存屏障,一般写项目的时候最好不要用volatile
。volatile
是给标准库和接近CLR的底层代码使用的。
编写应用程序代码时,正确使用锁和Interlocked
这类上层并发基元就能处理这些问题。
评论已关闭