如您所知,Microsoft 给出了以下示例,说明如果我们已经需要单独的 SetEvent 和 WaitForSingleObject (引用 Microsoft 示例),为什么我们可能需要 SignalObjectAndWait:
线程可以使用 SignalObjectAndWait 函数来确保工作线程在向对象发出信号之前处于等待状态。例如,一个线程和一个工作线程可以使用事件对象的句柄来同步它们的工作。该线程执行如下代码:
dwRet = WaitForSingleObject(hEventWorkerDone, INFINITE);
if( WAIT_OBJECT_0 == dwRet)
SetEvent(hEventMoreWorkToDo);
工作线程执行如下代码:
dwRet = SignalObjectAndWait(hEventWorkerDone,
hEventMoreWorkToDo,
INFINITE,
FALSE);
此算法流程存在缺陷,不应使用。我们不需要这样一个令人费解的机制,在我们处于“竞争条件”之前,线程会相互通知。在此示例中,Microsoft 自己创建了 Race Condition。工作线程应该只是等待事件并从列表中获取任务,而生成任务的线程应该只是将任务添加到该列表并发出事件信号。因此,我们只需要一个事件,而不是上面 Microsoft 示例中的两个事件。该列表必须由临界区保护。生成任务的线程不应等待工作线程完成任务。如果有任务需要在完成时通知某人,这些任务应该自己发送通知。换句话说,
这种有缺陷的设计,如 Microsoft 示例中所示,为诸如 atomic SignalObjectAndWait 和 atomic PulseEvent 之类的怪物创建了命令——最终导致厄运的函数。
这是一个算法,如何实现您在问题中设定的目标。只需简单的事件和简单的函数 SetEvent 和 WaitForSingleObject 即可实现目标 - 不需要其他函数。
- 为所有作业线程创建一个通用的自动重置事件,以表明有一个(任务)可用;并且还创建每个线程的自动重置事件,每个作业线程一个事件。
- 多个作业线程,一旦完成运行所有作业,都使用 WaitForMultipleObjects 等待这个常见的自动重置“任务可用”事件 - 它等待两个事件 - 公共事件和自己的线程事件。
- 调度程序线程将新的(待处理的)作业放到列表中。
- 作业列表访问必须由 EnterCriticalSection/LeaveCriticalSection 保护,因此没有人以其他方式访问此列表。
- 每个作业线程在完成一项作业后,在开始等待自动重置“任务可用”事件及其自己的事件之前,检查待处理作业列表。如果列表不为空,则从列表中获取一项作业(将其从列表中删除)并执行它。
- 必须有另一个受临界区保护的列表——等待作业线程列表。
- 在每个作业线程开始等待之前,即在它调用 WaitForMultipleObjects 之前,它会将自己添加到“等待”列表中。在退出等待时,它将自己从这个等待列表中删除。
- 当调度程序线程将新的(待处理的)作业放入作业列表时,它首先进入作业列表的关键部分,然后进入线程列表 - 因此同时进入两个关键部分。然而,作业线程可能永远不会同时进入两个临界区。
- 如果只有一个作业挂起,调度程序将公共自动重置事件设置为信号状态(调用 SetEvent)——哪个睡眠作业线程将接手该作业并不重要。
- 如果有两个或更多作业挂起,它不会发出公共事件的信号,但会计算有多少线程正在等待。如果至少有与作业一样多的线程在等待,则在有事件时用该数量的线程发出自己的事件,并让剩余的线程继续休眠。
- 如果作业多于等待线程,则为每个等待线程发出自己的事件信号。
- 在调度程序线程发出所有事件的信号后,它会离开临界区——首先是线程列表,然后是作业列表。
- 在调度程序线程发出特定情况所需的所有事件信号后,它自己进入睡眠状态,即使用自己的睡眠事件调用 WaitForSingleObject(这也是一个自动重置事件,每当出现新作业时都应该发出信号)。
- 由于作业线程在整个作业列表耗尽之前不会开始休眠,因此您将不再需要调度程序线程。只有在以后出现新作业时才需要调度程序线程,而不是在作业线程完成作业时。
重要提示:此方案纯粹基于自动重置事件。您永远不需要调用 ResetEvent。所有需要的函数是:SetEvent 和 WaitForMultipleObjects(或 WaitForSingleObject)。不需要原子事件操作。
请注意:当我写一个线程休眠时,它不会调用“休眠”API 调用——它永远不会被需要,它只是由于调用 WaitForMultipleObjects(或 WaitForSingleObject)而处于“等待”状态。
如您所知,自动复位事件以及 SetEvent 和 WaitForMultipleObjects 函数非常可靠。它们从 NT 3.1 开始存在。您可能总是构建这样一个仅依赖于这些简单函数的程序逻辑——因此您永远不需要假定原子操作的复杂且不可靠的函数,如 PulseEvent 或 SignalObjectAndWait。顺便说一句,SignalObjectAndWait 确实只出现在 Windows NT 4.0 中,而 SetEvent 和 WaitForMultipleObjects 确实存在于 Win32 的初始版本 – NT 3.1。