【问题标题】:Threads Waiting for Event Do Not Always Catch Event Signal等待事件的线程并不总是捕捉到事件信号
【发布时间】:2011-08-20 11:42:47
【问题描述】:

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

我的应用程序处理这些事件,这样当一个事件对象发出信号时,它的“所有者”线程负责重置事件对象的信号状态,如下面的代码 sn-p 所示。等待同一事件的其他线程不会尝试重置其信号状态。

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;
}

换句话说,我看到的问题似乎是Remarks section for PulseEvent on the MSDN website中描述的问题:

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

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

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

【问题讨论】:

  • 您是否期望在调用 SetEvent 时释放所有线程?按照设计,事件至少释放一个等待线程,除此之外,你不知道还有多少。可能不是全部。
  • @nos:MSDN for SetEvent 说“任何数量的等待线程,或随后通过调用其中一个等待函数为指定事件对象开始等待操作的线程,可以在对象的状态时释放已发出信号。”我将其解读为:如果事件保持设置,最终(双关语不是有意的:))所有等待的线程都会被释放。问题可能是被唤醒的线程重置了事件,所以一些等待的线程被释放而一些没有。
  • @Alexey Kukanov:我相信这正是我遇到的问题。

标签: c++ windows multithreading event-handling


【解决方案1】:

是的,有更好的方法:

[...] 改为使用条件变量。

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

具体寻找WakeAllConditionVariable

【讨论】:

  • 条件变量听起来不错,除了 MSDN 页面上的这条注释:“Windows Server 2003 和 Windows XP/2000:不支持条件变量。”不幸的是,我正在编写的应用程序必须与 Windows XP 兼容。你知道我可以实现条件变量的另一种方法吗?
【解决方案2】:

使用 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。对于这个解决方案,自动重置事件可能会更好。

【讨论】:

  • PulseEvent == 竞争条件。不要使用它。
  • @selbie 我知道 PulseEvent 是错误的,我不推荐它。也许我的评论不是很清楚(英语不是我的母语)所以请指出它有什么问题。
  • 当我看到“PulseEvent”并认为这是一个建议的解决方案时,我可能反应过度了。抱歉,我认为您关于条件变量的 cmets 是一个好的开始。
【解决方案3】:

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

自动重置事件为王!

PulseEvent 只出现在 Windows NT 4.0 中。它在最初的 Windows NT 3.1 中不存在。相反,CreateEvent、SetEvent 和WaitForMultipleObjects 等可靠函数从Windows NT 开始就存在,因此请考虑使用它们。

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

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

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

所以通过以下方式创建事件:

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

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

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

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

您可以在以下链接中找到有关内核模式异步过程调用的更多信息:

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

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

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

您在 theat 中写道“我能看到的唯一解决方案是让每个线程向该对象的所有者线程注册其对给定事件对象的使用”。这是正确的。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-08-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多