等待事件的线程并不总是捕获事件信号

Threads Waiting for Event Do Not Always Catch Event Signal

本文关键字:事件 信号 线程 等待      更新时间:2023-10-16

我有一个应用程序,其中多个线程等待同一个事件对象发出信号。 我看到的问题似乎是一种竞争条件,因为有时某些线程的等待状态(WaitForMultipleObjects)作为事件信号的结果返回,而其他线程的等待状态显然看不到事件信号,因为它们不返回。 这些事件是使用 CreateEvent 作为手动重置事件对象创建的。

我的应用程序处理这些事件,以便在事件对象发出信号时,其"所有者"线程负责重置事件对象的信号状态,如以下代码片段所示。 等待同一事件的其他线程不会尝试重置其信号状态。

switch ( dwObjectWaitState = ::WaitForMultipleObjects( i, pHandles, FALSE, INFINITE ) )
{
case WAIT_OBJECT_0 + BAS_MESSAGE_READY_EVT_ID:
    ::ResetEvent( pHandles[BAS_MESSAGE_READY_EVT_ID] );
    /* handles the event */
    break;
}

换句话说,我看到的问题似乎是MSDN网站上PulseEvent的"备注"部分中描述的内容:

如果发生对脉冲事件的调用 在线程具有 已从等待状态中删除, 线程不会被释放,因为 PulseEvent 仅释放这些线程 此刻正在等待 叫。因此,脉冲事件是 不可靠,不应由 新应用程序。相反,请使用 条件变量。

如果这是正在发生的事情,我能看到的唯一解决方案是让每个线程向该对象的所有者线程注册其对给定事件对象的使用,以便所有者线程可以确定何时可以安全地重置事件对象的信号状态。

有没有更好的方法可以做到这一点? 谢谢。

是的,有更好的方法:

[...]请改用条件变量。

http://msdn.microsoft.com/en-us/library/ms682052(v=vs.85).aspx

具体寻找WakeAllConditionVariable

为什么 PulseEvent() 不可靠,没有它该怎么办

自动重置事件为王!

PulseEvent只出现在Windows NT 4.0中。它在最初的Windows NT 3.1中不存在。 相反,像CreateEvent,SetEvent和WaitForMultipleObjects这样的可靠函数从Windows NT开始就存在,所以考虑使用它们。

CreateEvent 函数具有 bManualReset 参数。如果此参数为 TRUE,则该函数将创建一个手动重置事件对象,该对象需要使用 ResetEvent 函数将事件状态设置为非信号。这不是你需要的。如果此参数为 FALSE,则该函数将创建一个自动重置事件对象,系统在释放单个等待线程后自动将事件状态重置为非信号。

这些自动复位事件非常可靠且易于使用。

如果使用 WaitForMultipleObjects 或 WaitForSingleObject 等待自动重置事件对象,它会在退出这些等待函数时可靠地重置事件。

因此,请按以下方式创建事件:

EventHandle := CreateEvent(nil, FALSE, FALSE, nil);

等待来自一个线程的事件,然后从另一个线程执行 SetEvent。这非常简单且非常可靠。

永远不要调用ResetEvent(因为它会自动重置)或PulseEvent(因为它不可靠且已弃用)。甚至Microsoft也承认不应该使用PulseEvent。请参阅 https://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx

此函数不可靠,不应使用,因为在调用 PulseEvent 时,只会通知处于"等待"状态的那些线程。如果他们处于任何其他状态,则不会通知他们,并且您可能永远无法确定线程状态是什么。等待同步对象的线程可以通过内核模式异步过程调用暂时从等待状态中删除,然后在 APC 完成后返回到等待状态。如果在线程从等待状态中删除期间对 PulseEvent 的调用发生,则不会释放该线程,因为 PulseEvent 仅释放在调用时正在等待的那些线程。

可以通过以下链接了解有关内核模式异步过程调用的详细信息:

  • https://msdn.microsoft.com/en-us/library/windows/desktop/ms681951(v=vs.85).aspx
  • http://www.drdobbs.com/inside-nts-asynchronous-procedure-call/184416590
  • http://www.osronline.com/article.cfm?id=75

我们从未在我们的应用程序中使用过PulseEvent。至于自动重置事件,我们从Windows NT 3.51开始使用它们,并且它们运行良好。

当多个线程等待单个对象时该怎么办

不幸的是,您的情况有点复杂。您有多个线程在等待事件,并且必须确保所有线程实际上都收到了通知。除了为每个线程创建自己的事件之外,没有其他可靠的方法。

你写了"我能看到的唯一解决方案是让每个线程向该对象的所有者线程注册其对给定事件对象的使用"。这是正确的。

您还写道,"所有者线程可以确定何时可以安全地重置事件对象的信号状态" - 这是不切实际且不安全的。最好的方法是使用自动重置事件,这样它们就会自动重置。

因此,您需要拥有与线程一样多的事件。除此之外,您还需要保留已注册线程的列表。因此,要通知所有线程,您必须在所有事件句柄的循环中执行 SetEvent。这是一种非常快速,可靠和便宜的方法。事件比线程便宜得多。因此,线程数是一个问题,而不是事件数。内核对象几乎没有限制 - 内核句柄的每个进程限制为 2^24。

使用条件变量,如 PulseEvent 描述。唯一的问题是Windows上的本机条件变量是从Vista开始实现的,因此像XP这样的旧系统没有它。但是您可以使用其他一些同步对象(http://www1.cse.wustl.edu/~schmidt/win32-cv-1.html)模拟条件变量,但我认为最简单的方法是使用boost库中的条件变量及其notify_all方法来唤醒所有线程(http://www.boost.org/doc/libs/1_41_0/doc/html/thread/synchronization.html#thread.synchronization.condvar_ref)

另一种可能性(但不是很漂亮)是为每个线程创建一个事件,当现在你有PulseEvent时,你可以为所有线程调用SetEvent。对于此解决方案,自动重置事件可能会更好。