同步基元概述
更新:2007 年 11 月
.NET Framework 提供了一系列同步基元来控制线程交互并避免争用条件。这可大致分为三个类别:锁定、通知和联锁操作。
上述类别的定义并非是绝对的:有些同步机制具有多个类别的特征;一次释放一个线程的事件的功能类似于锁定;任何锁定的释放都可看作一个信号;而联锁操作可用于构造锁定。但是,这些类别仍然是有用的。
记住线程同步是协作这一点非常重要。只要有一个线程避开同步机制直接访问受保护的资源,该同步机制就不是有效的。
锁定
锁向一个线程一次提供一个资源的控制功能,或者向指定数目的线程提供此功能。请求正在使用中的独占锁的线程会被阻止,直到该锁变为可用为止。
独占锁
锁定的最简单的形式是 C# 的 lock 语句(在 Visual Basic 中为 SyncLock),该语句可控制对代码块的访问。这种块通常称为临界区。lock 语句使通过使用 Monitor 类的 Enter 和 Exit 方法实现的,它使用 try¡catch¡finally 确保该锁被释放。
通常情况下,使用 lock 语句保护小代码块并且不跨越多个方法是使用 Monitor 类的最佳方法。Monitor 类功能强大,但是容易形成孤立锁和死锁。
Monitor 类
Monitor 类提供了附加功能,可结合 lock 语句使用:
TryEnter 方法允许当前被阻止,正在等待资源的线程在指定时间间隔之后放弃。它返回一个指示成功或失败的布尔值,可用于检测和避免潜在的死锁。
Wait 方法由临界区中的线程调用。它放弃对资源的控制并阻止,直到该资源重新可用为止。
Pulse 和 PulseAll 方法允许要释放锁或调用 Wait 的线程将一个或多个线程放入就绪队列,以使它们能够获取锁。
Wait 方法重载的超时允许等待线程进入就绪队列。
如果用于锁的对象派生自 MarshalByRefObject,则 Monitor 类可在多个应用程序域中提供锁定。
Monitor 具有线程关联。也就是说,进入监视器的线程必须通过调用 Exit 或 Wait 才能退出。
Monitor 类不可实例化。其方法是静态(在 Visual Basic 中为 Shared)方法,用于可实例化的锁对象。
有关概念性概述,请参见 监视器。
Mutex 类
线程通过调用其 WaitOne 方法的重载请求 Mutex。提供了具有超时的重载,以便允许线程放弃等待。与 Monitor 类不同,mutex 可以是局部的,也可以是全局的。全局 mutex(也称为命名的 mutex)在整个操作系统中可见,可用于在多个应用程序域或进程中同步线程。局部 mutex 派生自 MarshalByRefObject,可以跨应用程序域边界使用。
此外,Mutex 派生自 WaitHandle,这意味着它可用于 WaitHandle 提供的通知机制,如 WaitAll、WaitAny 和 SignalAndWait 方法。
与 Monitor 一样,Mutex 具有线程关联。与 Monitor 不同,Mutex 是可实例化的对象。
有关概念性概述,请参见 Mutex。
其他锁
锁定不必是独占的。允许有限数目的线程并发访问某个资源通常十分有用。信号量和读写器锁旨在控制此类池资源访问。
ReaderWriterLock 类
ReaderWriterLockSlim 类用于更改数据的线程(编写器)必须独占访问某个资源的情形。如果编写器未处于活动状态,则任何数量的读取器均可以访问该资源(例如,通过调用 EnterReadLock 方法)。当某个线程请求独占访问时(例如,通过调用 EnterWriteLock 方法),后续读取器请求将被阻止,直至所有现有的读取器都已退出该锁,并且编写器也已进入并退出该锁。
ReaderWriterLockSlim 具有线程关联。
有关概念性概述,请参见 读取器/编写器锁。
Semaphore 类
Semaphore 类允许指定数目的线程访问某个资源。请求该资源的其他线程会一直阻止,直到某个线程释放信号量为止。
与 Mutex 类一样,Semaphore 派生自 WaitHandle。Semaphore 也与 Mutex 一样,可以是局部的,也可以是全局的。它可以跨应用程序域边界使用。
与 Monitor、Mutex 和 ReaderWriterLock 不一样,Semaphore 不具有线程关联。这意味着它可以用于一个线程获取信号量而另一个线程释放该信号量的情形。
有关概念性概述,请参见 信号量。
通知
等待另一个线程的信号的最简单方法是调用 Join 方法,该方法会一直阻止,直到另一个线程完成为止。Join 有两个重载方法,这两个方法允许阻止的线程在等待指定时间间隔之后跳出等待。
等待句柄提供了更为丰富的等待和通知功能。
等待句柄
等待句柄派生自 WaitHandle 类,后者又派生自 MarshalByRefObject。因此,等待句柄可用于跨应用程序域边界同步线程的活动。
通过调用实例方法 WaitOne 或者静态方法 WaitAll、WaitAny 或 SignalAndWait 中的一个方法,线程可由等待句柄阻止。它们的释放方式取决于调用的方法以及等待句柄的种类。
有关概念性概述,请参见 等待句柄。
事件等待句柄
事件等待句柄包括 EventWaitHandle 类及其派生类 AutoResetEvent 和 ManualResetEvent。当通过调用 Set 方法或使用 SignalAndWait 方法通知事件等待句柄时,线程会从事件等待句柄释放。
事件等待句柄要么自动重置自身(类似于每次得到通知时只允许一个线程通过的旋转门),要么必须手动重置(类似于在通知前一直关闭,有人将其关闭前则一直打开的大门)。顾名思义,AutoResetEvent 和 ManualResetEvent 分别表示前者和后者。
EventWaitHandle 可表示这两种类型的事件,并且既可以是局部的也可以是全局的。派生类 AutoResetEvent 和 ManualResetEvent 始终是局部的。
事件等待句柄不具有线程关联。任何线程都可以通知事件等待句柄。
有关概念性概述,请参见 EventWaitHandle、AutoResetEvent 和 ManualResetEvent。
Mutex 和 Semaphore 类
因为 Mutex 和 Semaphore 类派生自 WaitHandle,所以它们可用于 WaitHandle 的静态方法。例如,线程可以使用 WaitAll 方法等待,直到满足以下三个条件为止:EventWaitHandle 接收到通知,Mutex 已释放,Semaphore 已释放。类似地,线程可以使用 WaitAny 方法等待,直到满足上述所有条件为止。
对于 Mutex 或 Semaphore,接收到通知即意味着被释放。如果上述两个类型之一用作 SignalAndWait 方法的第一个参数,该类型即被释放。对于具有线程关联的 Mutex,如果进行调用的线程不具有该 mutex,则会引发异常。如前所述,信号量不具有线程关联。
联锁操作
联锁操作是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换,以及 32 位平台上的 64 位值的读取操作。
说明: |
---|
原子性的保证仅限于单个操作;如果必须将多个操作作为一个单元执行,则必须使用更粗粒度的同步机制。 |
尽管这些操作中没有一个是锁或信号,但它们可用于构造锁和信号。因为它们是 Windows 操作系统固有的,因此联锁操作的执行速度非常快。
联锁操作可用于可变内存保证,以编写展示功能强大的非阻止并发的应用程序,但是,它们需要复杂的低级别编程,因此大多数情况下简单锁是更好的选择。
有关概念性概述,请参见 互锁操作。