欲了解更多信息,请参阅J. Albahari 的很棒的网站 http://www.albahari.com/threading/part4.aspx#_The_volatile_keyword:
同步结构可以分为四类:
简单的拦截方法:
它们等待另一个线程完成或经过一段时间。Sleep
, Join
, and Task.Wait
是简单的阻塞方法。
锁定结构:
这些限制了一次可以执行某些活动或执行一段代码的线程数量。独占锁定结构是最常见的——它们一次只允许一个线程进入,并允许竞争线程访问公共数据而不会互相干扰。标准的独占锁定结构是lock
(Monitor.Enter
/Monitor.Exit
), Mutex
, and SpinLock
。非排他性的locking
构造是Semaphore
, SemaphoreSlim
,以及reader/writer
locks.
信号传导结构:
这些允许线程暂停,直到收到另一个线程的通知,从而避免低效的轮询。有两种常用的信号设备:事件等待句柄和监视器的等待/脉冲方法。框架4.0引入了CountdownEvent
and Barrier
类。
非阻塞同步构造:
它们通过调用处理器原语来保护对公共字段的访问。 CLR 和 C# 提供以下非阻塞构造:Thread.MemoryBarrier
, Thread.VolatileRead
, Thread.VolatileWrite
, the volatile
关键字,以及Interlocked
class.
The volatile
关键词:
volatile 关键字指示编译器在每次读取该字段时生成一个获取栅栏,并在每次写入该字段时生成一个释放栅栏。获取栅栏可防止其他读/写在栅栏之前移动;释放栅栏可防止其他读/写在栅栏之后移动。这些“半栅栏”比全栅栏更快,因为它们为运行时和硬件提供了更多优化空间。
碰巧的是,Intel 的 X86 和 X64 处理器总是对读取应用获取栅栏,对写入应用释放栅栏 — 无论您是否使用 volatile 关键字 — 因此,如果您使用这些处理器,则该关键字对硬件没有影响。然而,volatile
确实会对编译器和 CLR 以及 64 位 AMD 和(在更大程度上)安腾处理器执行的优化产生影响。这意味着您不能因为您的客户端运行特定类型的 CPU 而更加放松。
将 volatile 应用于字段的效果可以总结如下:
First instruction Second instruction Can they be swapped?
Read Read No
Read Write No
Write Write No (The CLR ensures that write-write operations are never swapped, even without the volatile keyword)
Write Read Yes!
请注意,应用 volatile 并不会阻止先写后读的交换,这可能会造成脑筋急转弯。 Joe Duffy 用下面的例子很好地说明了这个问题:如果Test1
and Test2
在不同的线程上同时运行,a 和 b 的值有可能都为 0(尽管两者都使用了 volatile)x
and y
):
class IfYouThinkYouUnderstandVolatile
{
volatile int x, y;
void Test1() // Executed on one thread
{
x = 1; // Volatile write (release-fence)
int a = y; // Volatile read (acquire-fence)
...
}
void Test2() // Executed on another thread
{
y = 1; // Volatile write (release-fence)
int b = x; // Volatile read (acquire-fence)
...
}
}
MSDN 文档指出,使用 volatile 关键字可确保字段中始终存在最新值。这是不正确的,因为正如我们所看到的,先写后读可以重新排序。
这为避免 volatile 提供了强有力的理由:即使您理解此示例中的微妙之处,其他处理您代码的开发人员也会理解它吗?两个作业之间的完整栅栏Test1
and Test2
(或锁)解决了问题。
The volatile
关键字不支持传递引用参数或捕获的局部变量:在这些情况下,您必须使用VolatileRead
and VolatileWrite
方法。