Auto 和 Manual Reset Events 再探






4.91/5 (18投票s)
本文介绍何时使用自动重置事件和手动重置事件。
引言
在多线程场景中,程序员通常会使用自动重置事件和手动重置事件进行异步通知。然而,大多数人在选择正确的事件类型时都比较天真。许多网站都详细描述了事件对象,但仅限于其定义。本文将深入探讨如何以及何时使用它们,以及它们能为我们提供什么。
背景
本文假设读者了解内核对象和同步原语。本文不涉及事件对象以外的任何同步原语。本文介绍的是 Windows 操作系统中的事件对象。
事件对象类型
Windows 提供了两种事件类型:
- 自动重置事件,一旦设置为有信号状态,在至少一个等待的线程被释放后,会自动重置为无信号状态。
- 另一方面,手动重置事件一旦设置为有信号状态,就需要显式重置才能变为无信号状态。
重要的是要记住,事件不遵循所有权概念,即在同一线程上连续调用同一事件对象的等待将是阻塞调用,这与互斥量、临界区或信号量不同。此外,任何线程都可以发出事件对象的信号,就像任何线程都可以等待事件对象发出信号一样。
文章的以下部分将阐述这两种事件之间的区别以及何时使用哪种事件。
自动重置事件
为了更好地用例子来说明,假设有三个线程 A、B 和 C 正在等待一个自动重置事件 AE。当 AE 被信号(设置)时,操作系统调度程序会原子地执行以下三个操作:
- 发出事件信号。
- 释放一个等待的线程 - 在线程 A、B 或 C 中只有一个线程会从等待状态释放。其他线程将继续处于等待状态,等待该事件发出信号。
- 重置事件。
未被释放的线程不知道事件已被发出信号并重置。程序员无法控制要从等待状态释放哪个线程。这完全取决于操作系统的调度程序。由于这种非确定性,自动重置事件通常用于线程之间的同步,其中一个线程等待事件发出信号,而另一个或多个线程发出事件信号。被释放的等待线程在完成其任务后,通常会返回等待状态(以接收相同自动重置事件的通知)。
用例
自动重置事件的一个非常常见的用途是任务分派器。任务分派器维护一个待分派的任务队列。任务队列由分派器线程检查并(一次一个)分派。一旦队列变空,分派器线程就会进入等待状态,等待自动重置事件发出信号。每次将项目添加到队列时,都会发出此自动重置事件的信号。这样,当一个任务入队时,分派器线程就会唤醒,分派任务(直到队列变空),然后返回等待状态。
请注意,只有一个等待实体和一个或多个信号实体。在上面的例子中,任务分派器线程是等待实体,而添加任务到队列的其他线程是信号实体。
自动重置事件的另一个好例子是,它可以用于实现线程池机制,其中一组线程可以等待单个自动重置事件(与通常只有一个线程等待的情况不同)。当一个工作项入队时,会自动重置事件,这只会释放线程池中的一个等待线程,该线程随后出队并执行工作项。
手动重置事件
在发出手动重置事件信号后,所有等待该事件的线程都将从等待状态释放。
例如,假设有三个线程 A、B 和 C 正在等待一个手动重置事件 MRE。当 MRE 被信号(设置)时,操作系统调度程序会原子地执行以下操作:
- 发出事件信号。
- 释放线程 - 当前等待该 MRE 的所有线程都将被释放。
请注意与自动重置事件在顺序上的区别。第三步,即重置事件,留给程序员来完成。事件将保持有信号状态,直到程序员显式重置它。在此事件被重置之前,任何后续的等待都将立即返回。
手动重置事件适用于需要将单个事件通知多个线程的情况。
用例
手动重置事件的一个典型用例是关闭同步。假设您有 'n' 个工作线程,并且希望确保一个干净的关闭过程,您可以让所有 'n' 个线程等待一个通用的手动重置事件对象。当要启动关闭过程时,只需发出手动重置事件的信号,等待 'n' 个线程退出,然后重置事件即可。但是,等待线程退出有一个警告。有些线程可能永远不会返回(可能卡在死锁中)。一种方法是等待线程退出一定时间,如果它们不退出则终止它们。这些都是需要在设计中考虑的问题。
何时调用 Reset?
这个问题的答案是通过设计选择来确定的,必须了解 Reset 调用放置位置的后果。错误的选择可能导致灾难。
- Set 之后的 Reset
- 当您发出事件信号时,所有可等待的线程都处于等待状态。这不一定是真的,因为其中一些线程可能已经退出了之前的等待,并在返回等待状态之前执行了一些代码。在这种情况下,如果 Reset 调用发生在这些线程返回等待状态之前,这些线程将错过该事件的发生。这会在系统中引入不可预测性。
- 在 Set 和(立即)Reset 调用之间创建的任何新线程将不会等待,因为事件处于有信号状态。这可能是一个设计选择,但无论如何,应该评估这种情况。
- 外部触发的 Reset
- 不调用 Reset
- 基于其他复杂逻辑的 Reset
以下假设必须做出:
在这种情况下,手动重置事件的行为类似于一个外部控制的开关。例如,可以使用手动重置事件来实现暂停/恢复机制,用于一组任务,每个任务都有相应的线程来执行。同样,必须考虑在 Reset 调用之后是否需要确保所有活动都已停止。
如果已知特定事件只发生一次,或者下一次发生对系统无关紧要,则可以使用此方法。这可以应用于本文前面引用的关闭处理示例。
如果 Reset 基于其他复杂逻辑,建议寻找替代的同步原语,因为很难避免使用手动重置事件可能发生的竞争条件。
结论
事件对象有助于以简单的方式解决各种同步问题。但务必注意为给定情况选择了错误的事件类型。不幸的是,大多数程序员都被手动重置事件的显式重置控制所吸引,而这在大多数情况下并不适合他们的场景。这会导致软件中出现竞争条件和其他不可预测的行为。希望本文能帮助程序员理解并选择适合其场景的事件对象类型。
历史
- 2009 年 8 月 17 日 – 初稿。