.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 。
SpinLock 类
其他锁
锁不必是独占的。 允许有限数目的线程并发访问某个资源通常十分有用。 信号量和读写器锁旨在控制此类池资源访问。
ReaderWriterLock 类
ReaderWriterLockSlim 类用于更改数据的线程(编写器)必须独占访问某个资源的情形。 如果编写器未处于活动状态,则任何数量的读取器均可以访问该资源(例如,通过调用 EnterReadLock 方法)。 当某个线程请求独占访问时(例如,通过调用 EnterWriteLock 方法),后续读取器请求将被阻止,直至所有现有的读取器都已退出该锁,并且编写器也已进入并退出该锁。
ReaderWriterLockSlim 具有线程关联。
有关概念性概述,请参见 读取器/编写器锁 。
Semaphore 类
Semaphore 类允许指定数目的线程访问某个资源。 请求该资源的其他线程会一直阻止,直到某个线程释放信号量为止。
与 Mutex 类一样, Semaphore 派生自 WaitHandle 。 Semaphore 也与 Mutex 一样,可以是局部的,也可以是全局的。 它可以跨应用程序域边界使用。
与 Monitor 、 Mutex 和 ReaderWriterLock 不一样, Semaphore 不具有线程关联。 这意味着它可以用于一个线程获取信号量而另一个线程释放该信号量的情形。
有关概念性概述,请参见 Semaphore 和 SemaphoreSlim 。
System.Threading :: SemaphoreSlim 是一个用于在单一进程边界内进行同步的轻量信号量。
等待来自另一个线程的信号的最简单的方法是调用 Join 方法,该方法将进行阻塞,直至其他线程完成。 Join 具有两个允许阻塞的线程在经过指定时间间隔后停止等待的重载。
等待句柄提供了更为丰富的等待和通知功能。
等待句柄
等待句柄派生自 WaitHandle 类,后者又派生自 MarshalByRefObject 。 因此,等��句柄可用于跨应用程序域边界同步线程的活动。
通过调用实例方法 WaitOne 或者静态方法 WaitAll 、 WaitAny 或 SignalAndWait 中的一个方法,线程可由等待句柄阻止。 它们的释放方式取决于调用的方法以及等待句柄的种类。
有关概念性概述,请参见 等待句柄 。
事件等待句柄
事件等待句柄包括 EventWaitHandle 类及其派生类 AutoResetEvent 和 ManualResetEvent 。 当通过调用 Set 方法或使用 SignalAndWait 方法通知事件等待句柄时,线程会从事件等待句柄释放。
事件等待句柄要么自动重置自身(类似于每次得到通知时只允许一个线程通过的旋转门),要么必须手动重置(类似于在通知前一直关闭,有人将其关闭前则一直打开的大门)。 顾名思义, AutoResetEvent 和 ManualResetEvent 分别表示前者和后者。 System.Threading :: ManualResetEventSlim 是一个用于在单一进程边界内进行同步的轻量事件。
EventWaitHandle 可表示这两种类型的事件,并且既可以是局部的也可以是全局的。 派生类 AutoResetEvent 和 ManualResetEvent 始终是局部的。
事件等待句柄不具有线程关联。 任何线程都可以通知事件等待句柄。
有关概念性概述,请参见 EventWaitHandle、AutoResetEvent、CountdownEvent 和 ManualResetEvent 。
Mutex 和 Semaphore 类
因为 Mutex 和 Semaphore 类派生自 WaitHandle ,所以它们可用于 WaitHandle 的静态方法。 例如,线程可以使用 WaitAll 方法等待,直到满足以下三个条件为止: EventWaitHandle 接收到通知, Mutex 已释放, Semaphore 已释放。 类似地,线程可以使用 WaitAny 方法等待,直到满足上述所有条件为止。
对于 Mutex 或 Semaphore ,接收到通知即意味着被释放。 如果上述两个类型之一用作 SignalAndWait 方法的第一个参数,该类型即被释放。 对于具有线程关联的 Mutex ,如果进行调用的线程不具有该 mutex,则会引发异常。 如前所述,信号量不具有线程关联。
关卡
利用 Barrier 类,可以对多个线程进行循环同步,以便它们都在同一个点上阻塞并等待所有其他线程完成。 对于一个或多个线程在继续某个算法的下一阶段之前需要另一个线程的结果的情况,关卡很有用。 有关更多信息,请参见 屏障 (.NET Framework) 。
从 .NET Framework 4 开始,可以使用同步基元,通过尽可能避免依赖高开销的 Win32 内核对象(例如等待句柄)来提高性能。 通常,当等待时间较短并且只有在尝试了原始同步类型并发现它们并不令人满意时,才应使用这些类型。 在需要跨进程通信的方案中不能使用轻量类型。
-
System.Threading :: SemaphoreSlim 是 System.Threading :: Semaphore 的轻量版本。
-
System.Threading :: ManualResetEventSlim 是 System.Threading :: ManualResetEvent 的轻量版本。
-
System.Threading :: CountdownEvent 表示一个事件,当它的计数为零时,该事件将发出信号。
-
System.Threading :: Barrier 使多个线程能够在彼此之间进行同步,而不需要由主线程进行控制。 在所有线程已到达指定点之前,关卡会防止每个线程继续。
从 .NET Framework 4 开始,当线程必须等待发生某个事件发出信号时或需要满足某个条件时,可以使用 System.Threading :: SpinWait 结构,但前提是实际等待时间预计会少于通过使用等待句柄或通过其他方式阻塞当前线程所需要的等待时间。 通过使用 SpinWait ,可以指定在一个较短的时段内边等待边旋转,然后只有在相应的条件在指定时间内无法得到满足的情况下放弃旋转(例如,通过等待或休眠)。
联锁操作是由 Interlocked 类的静态方法对某个内存位置执行的简单原子操作。 这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换,以及 32 位平台上的 64 位值的读取操作。
原子性的保证仅限于单个操作;如果必须将多个操作作为一个单元执行,则必须使用更粗粒度的同步机制。 |
尽管这些操作中没有一个是锁或信号,但它们可用于构造锁和信号。 因为它们是 Windows 操作系统固有的,因此联锁操作的执行速度非常快。
联锁操作可与易失存储器保证一起使用,以编写展示强大的非阻塞并发功能的应用程序。 但是,它们需要复杂的低级别编程,因此大多数情况下,简单的锁定是更好的选择。
有关概念性概述,请参见 互锁操作 。