时间:2021-02-13 11:14:44 | 栏目:.NET代码 | 点击:次
以前我们说过在一些简单的例子中,比如为一个字段赋值或递增该字段,我们需要对线程进行同步,
虽然lock可以满足我们的需要,但是一个竞争锁一定会导致阻塞,然后忍受线程上下文切换和调度的开销,在一些高并发和性能比较关键的地方,这些是不能忍受的。
.net framework 提供了非阻塞同步构造,为一些简单的操作提高了性能,它甚至都没有阻塞,暂停,和等待线程。
Memory Barriers and Volatility (内存栅栏和易失字段 )
考虑下下面的代码:
回答是“yes”,基于以下原因:
编译器,clr 或 cpu 可能会为了性能而重新为程序的指令进行排序,例如可能会将方法A中的两句代码的顺序进行调整。
编译器,clr 或 cpu 可能会为变量的赋值采用缓存策略,这样这些变量就不会立即对其他变量可见了,例如方法A中的变量赋值,不会立即刷新到内存中,变量B看到的变量并不是最新的值。
C# 和运行时非常小心的保证这些优化策略不会影响正常的单线程的代码和在多线程环境下加锁的代码。
除此之外,你必须显示的通过创建内存屏障(Memory fences) 来限制指令重新排序和读写缓存对程序造成的影响。
Full fences:
最简单的完全栅栏的方法莫过于使用Thread.MemoryBarrier方法了。
以下是msdn的解释:
Thread.MemoryBarrier: 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时,不能采用先执行 MemoryBarrier 调用之后的内存访问,再执行 MemoryBarrier 调用之前的内存访问的方式。
按照我个人的理解:就是写完数据之后,调用MemoryBarrier,数据就会立即刷新,另外在读取数据之前调用MemoryBarrier可以确保读取的数据是最新的,并且处理器对MemoryBarrier的优化小心处理。
C# Lock 语句(Monitor.Enter / Monitor.Exit)
在Interlocked类的所有方法。
使用线程池的异步回调,包括异步的委托,APM 回调,和 Task continuations.
在一个信号构造中的发送(Settings)和等待(waiting)
你不需要对每一个变量的读写都使用完全栅栏,假设你有三个answer 字段,我们仍然可以使用4个栅栏。例如:
第一条指令
|
第二条指令
|
可以被交换吗?
|
Read
|
Read
|
No
|
Read
|
Write
|
No
|
Write
|
Write
|
No(CLR会确保写和写的操作不被交换,甚至不使用volatile关键字)
|
Write
|
Read
|
Yes! |
void Test2()
{
y = 1; //Volatile write
int b = x; //Volatile Read
}
这是一个避免使用volatile关键字的好例子,甚至假设你彻底的明白了这段代码,是不是其他在你的代码上工作的人也全部明白呢?。
在Test1 和Test2方法中使用完全栅栏或者是lock都可以解决这个问题,
还有一个不使用volatile关键字的原因是性能问题,因为每次读写都创建了内存栅栏,例如
VolatileRead和VolatileWrite方法。例如
volatile int m_amount;
Boolean success =int32.TryParse(“123”,out m_amount);
//生成如下警告信息:
//cs0420:对volatile字段的引用不被视为volatile.
从技术上讲,Thread类的静态方法VolatileRead和VolatileWrite在读取一个 变量上和volatile 关键字的作用一致。
他们的实现是一样是低效率的,尽管事实上他们都创建了内存栅栏。下面是他们在integer类型上的实现。
public static int VolatileRead(ref int address)
{
int num = address; Thread.MemoryBarrier(); return num;
}