深入浅出多线程系列之八:内存栅栏和volatile 关键字
以前我们说过在一些简单的例子中,比如为一个字段赋值或递增该字段,我们需要对线程进行同步,
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.net framework 提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。
Memory Barriers and Volatility (内存栅栏和易失字段 )
考虑下下面的代码:
int _answer;
bool _complete;void A()
{
_answer = 123;
_complete = true;
}void B()
{
if (_complete)
Console.WriteLine(_answer);
}
如果方法A和B都在不同的线程下并发的执行,方法B可能输出 “0” 吗?
回答是“yes”,基于以下原因:
编译器,clr 或 cpu 可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
编译器,clr 或 cpu 可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。
C# 和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memory fences) 来限制指令重新排序和读写缓存对程序造成的影响。
Full fences:
最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。
以下是msdn的解释:
Thread.MemoryBarrier: 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存访问,再执行 MemoryBarrier 调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。
int _answer;
bool _complete;void A()
{
_answer = 123;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier();//在写完之后,创建内存栅栏
}void B()
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier();//在读取之前,创建内存栅栏
Console.WriteLine(_answer);
}
}
一个完全的栅栏在现代桌面应用程序中,大于需要花费10纳秒。
下面的一些构造都隐式的生成完全栅栏。
C# Lock 语句(Monitor.Enter / Monitor.Exit)
在Interlocked类的所有方法。
使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.
在一个信号构造中的发送(Settings)和等待(waiting)
你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer 字段,我们仍然可以使用4个栅栏。例如:
int _answer1, _answer2, _answer3;
bool _complete;void A()
{
_answer1 = 1; _answer2 = 2; _answer3 = 3;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
_complete = true;
Thread.MemoryBarrier(); //在写完之后,创建内存栅栏
}void B()
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
if (_complete)
{
Thread.MemoryBarrier(); //在读取之前,创建内存栅栏
Console.WriteLine(_answer1 + _answer2 + _answer3);
}
}
我们真的需要lock 和内存栅栏吗?
在一个共享可写的字段上不使用lock 或者栅栏 就是在自找麻烦,在msdn上有很多关于这方面的主题。
考虑下下面的代码:
public static void Main()
{
bool complete = false;
var t = new Thread(() =>
{
bool toggle = false;
while (!complete) toggle = !toggle;
});
t.Start();
Thread.Sleep(1000);
complete = true;
t.Join();
}
如果你在Visual Studio中选择发布模式,生成该应用程序,那么如果你直接运行应用程序,程序都不会中止。
因为CPU 寄存器把 complete 变量的值给缓存了。在寄存器中,complete永远都是false。
通过在while循环中插入Thread.MemoryBarrier,或者是在读取complete的时候加锁 都可以解决这个问题。
volatile 关键字
为_complete字段加上volatile关键字也可以解决这个问题。
volatile bool _complete.
Volatile关键字会指导编译器自动的为读写字段加屏障.以下是msdn的解释:
volatile 关键字指示一个字段可以由多个同时执行的线程修改。声明为 volatile 的字段不受编译器优化(假定由单个线程访问)的限制。这样可
补充:综合编程 , 其他综合 ,