同步基元概述

网友投稿 649 2022-08-25

同步基元概述

同步基元概述

.NET Framework 提供了一系列同步基元来控制线程交互并避免争用条件。这可大致分为三个类别:锁定、通知和联锁操作。

上述类别的定义并非是绝对的:有些同步机制具有多个类别的特征;一次释放一个线程的事件从功能上来说类似于锁;任何锁的释放都可看作一个信号;而联锁操作可用于构造锁。但是,这些类别仍然是有用的。

记住线程同步是协作这一点非常重要。只要有一个线程避开同步机制直接访问受保护的资源,该同步机制就不是有效的。

本概述包含以下几节:

​​锁定​​​​通知​​​​轻量同步类型​​​​SpinWait​​​​联锁操作​​

锁定

锁向一个线程一次提供一个资源的控制功能,或者向指定数目的线程提供此功能。请求正在使用中的独占锁的线程会被阻止,直到该锁变为可用为止。

独占锁

锁定的最简单的形式是 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 类

从 .NET Framework 4 版开始,当 ​​Monitor​​​ 所需的开销会造成性能下降时,可以使用 ​​SpinLock​​​ 类。当 ​​SpinLock​​​ 遇到锁定的临界区时,它只是反复地旋转,直至锁变为可用的。如果锁保留的时间非常短,则旋转可比阻塞提供更好的性能。但是,如果锁保留数十个周期以上,则 ​​SpinLock​​​ 的表现会和 ​​Monitor​​ 一样,只不过将使用更多的 CPU 周期,因此会降低其他线程或进程的性能。

其他锁

锁不必是独占的。允许有限数目的线程并发访问某个资源通常十分有用。信号量和读写器锁旨在控制此类池资源访问。

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​​ 使多个线程能够在彼此之间进行同步,而不需要由主线程进行控制。在所有线程已到达指定点之前,关卡会防止每个线程继续。

​​返回页首​​

SpinWait

从 .NET Framework 4 开始,当线程必须等待发生某个事件发出信号时或需要满足某个条件时,可以使用 ​​System.Threading::SpinWait​​​ 结构,但前提是实际等待时间预计会少于通过使用等待句柄或通过其他方式阻塞当前线程所需要的等待时间。通过使用 ​​SpinWait​​,可以指定在一个较短的时段内边等待边旋转,然后只有在相应的条件在指定时间内无法得到满足的情况下放弃旋转(例如,通过等待或休眠)。

​​返回页首​​

联锁操作

联锁操作是由 ​​Interlocked​​ 类的静态方法对某个内存位置执行的简单原子操作。这些原子操作包括添加、递增和递减、交换、依赖于比较的条件交换,以及 32 位平台上的 64 位值的读取操作。


说明

原子性的保证仅限于单个操作;如果必须将多个操作作为一个单元执行,则必须使用更粗粒度的同步机制。

尽管这些操作中没有一个是锁或信号,但它们可用于构造锁和信号。因为它们是 Windows 操作系统固有的,因此联锁操作的执行速度非常快。

联锁操作可与易失存储器保证一起使用,以编写展示强大的非阻塞并发功能的应用程序。但是,它们需要复杂的低级别编程,因此大多数情况下,简单的锁定是更好的选择。

有关概念性概述,请参见 ​​互锁操作​​。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:多线程服务器的常用编程模型
下一篇:K-th Nya Number (数位dp+二分)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~