【问题标题】:What are alternatives to Win32 PulseEvent() function?Win32 PulseEvent() 函数有哪些替代方法?
【发布时间】:2011-02-13 02:54:57
【问题描述】:

Win32 API PulseEvent() 函数 (kernel32.dll) 的文档指出该函数“……不可靠,不应由新应用程序使用。相反,使用条件变量”。但是,条件变量不能像(命名的)事件那样跨流程边界使用。

我有一个跨进程、跨运行时(本机和托管代码)的场景,其中单个生产者偶尔会向 个或更多消费者提供一些有趣的东西。现在,生产者使用这个 PulseEvent 函数使用(并设置为有信号状态)一个众所周知的命名事件,当它需要让某些事情知道时。零个或多个消费者等待该事件(WaitForSingleObject()) 并执行操作作为响应。在我的场景中不需要双向通信,生产者不需要知道事件是否有任何侦听器,也不需要知道需要知道事件是否被成功执行。另一方面,我不希望任何消费者永远错过任何事件。换句话说,系统需要完全可靠 - 但生产者不需要知道是不是这样。这个场景可以被认为是一个“时钟计时器”——即生产者为零个或多个消费者提供一个半规则的信号来计数。所有消费者必须在任何给定时间段内都有正确的计数。不允许消费者进行轮询(性能原因)。代码只有几毫秒(20 左右,但不是完全正常)。

Raymen Chen(旧新事物)has a blog post 指出 PulseEvent() 函数的“根本缺陷”性质,但我没有看到 Chen 或发布的 cmets 的替代方案。

谁能推荐一个?

请记住,IPC 信号必须跨越机器上的进程边界,而不仅仅是线程。并且解决方案需要具有高性能,因为消费者必须能够在每个事件的 10 毫秒内采取行动。

【问题讨论】:

    标签: winapi synchronization ipc


    【解决方案1】:

    我认为您将需要一些更复杂的东西来达到您的可靠性目标。

    我对您的问题的理解是,您有一个生产者和未知数量的消费者,它们都是不同的过程。每个消费者都不会错过任何事件。

    我想更清楚地了解缺少事件的含义。

    i) 如果消费者在等待您的通知方法之前开始运行并到达,并且发生了一个事件,即使在发送通知时它还没有完全准备好,它是否应该处理它? (即消费者何时被认为是活跃的?何时开始或何时处理其第一个事件)

    ii) 同样,如果消费者正在处理一个事件并且等待下一个通知的代码还没有开始等待(我假设一个Wait -> Process -> Loop to Wait 代码结构)那么它是否应该知道另一个事件发生了当它在循环时?

    我假设 i) 是“不是真的”,因为它是进程启动和“准备好”之间的竞赛,并且 ii) 是“是”;也就是说,一旦消费者出现,通知就会有效地按消费者排队,并且每个消费者都可以消费在其处于活动状态时产生的所有事件,并且不会跳过任何事件。

    因此,您所追求的是向一组消费者发送通知流的能力,其中消费者可以保证对该流中的所有通知采取行动,从它作用于第一个点到它关闭了。即如果生产者产生以下通知流

    1 2 3 4 5 6 7 8 9 0

    消费者 a) 启动并处理 3,它也应该处理 4-0

    如果消费者 b) 启动并处理 5,但在 9 后关闭,那么它应该已经处理了 5,6,7,8,9

    如果消费者 c) 在通知开始时正在运行,它应该已处理 1-0

    等等

    简单地触发一个事件是行不通的。如果在事件发生脉冲时消费者没有主动等待事件,那么它将错过该事件,因此如果事件产生的速度超过我们可以循环再次等待事件的速度,我们将失败。

    使用信号量也不会起作用,就好像一个消费者比另一个消费者运行得快,以至于它可以在另一个消费者完成处理之前循环到信号量调用,如果在这段时间内有另一个通知,那么一个消费者可以处理一个事件不止一次,一个人可能会错过一个。也就是说,您可能会释放 3 个线程(如果生产者知道有 3 个消费者),但您不能确保每个消费者只释放一次。

    共享内存中的事件(滴答计数)环形缓冲区,每个消费者都知道它上次处理的事件的值,并且消费者通过脉冲事件发出警报,这应该以一些消费者不同步为代价有时蜱;也就是说,如果他们错过了一个,他们会在下次脉搏时赶上。只要环形缓冲区足够大,以便所有消费者都可以在生产者在缓冲区中循环之前处理事件,就可以了。

    在上面的例子中,如果消费者 d 错过了事件 4 的脉冲,因为它当时没有等待它的事件,然后它进入等待状态,它会在事件 5 产生时被唤醒,因为它是最后一次处理的计数为 3,它将处理 4 和 5,然后循环回事件...

    如果这还不够好,那么我会建议类似PGM 通过套接字为您提供可靠的多播;这样做的好处是您可以将您的消费者转移到不同的机器上......

    【讨论】:

    • 谢谢。你已经很好地描述了我的情况。事实上,这正是我现在所拥有的——共享内存中的一个计数器,一个带有事件数据的环形缓冲区,以及一个“知道”它是否偶尔错过事件的消费者,因为它在 PulseEvent 发生时没有等待。我在 StackOverflow 上“钓鱼”,看看我能不能做得更好。我们每周错过大约 1 或 2 个事件(24/7 运行,每个事件 20 毫秒和大约 10 个消费者),这几乎是完美的。无法对“错过的事件”采取行动 - 它仅在短时间内有效。
    • PGM 可能会给您带来太多延迟。鉴于无法对错过的事件采取行动,我很想说您正在按预期进行操作。如果消费者在新事件发生时还没有为新事件做好准备并且无法赶上,那么它所能做的当然就是跳过该事件,这就是正在发生的事情。
    • 此视频 (channel9.msdn.com/shows/Going+Deep/…) 解释了为什么 Pulse 不可靠且已弃用。
    【解决方案2】:

    PulseEvent 存在两个固有问题:

    • 如果它与自动重置事件一起使用,它只会释放一个服务员。
    • 如果线程在PulseEvent 的时刻由于 APC 而从等待队列中删除,则它们可能永远不会被唤醒。

    另一种方法是广播一个窗口消息,并让任何侦听器都有一个仅用于侦听此特定消息的顶级消息窗口。

    这种方法的主要优点是您不必显式阻塞线程。这种方法的缺点是您的侦听器必须是 STA(不能在 MTA 线程上拥有消息队列)。

    这种方法的最大问题是侦听器对事件的处理将随着队列获取该消息所需的时间而延迟。

    您还可以确保使用手动重置事件(以便唤醒所有等待的线程)并执行 SetEvent/ResetEvent 并稍稍延迟(例如 150 毫秒),以便为临时唤醒的线程提供更大的机会APC 接您的活动。

    当然,这些替代方法是否适合您取决于您​​需要触发事件的频率,以及您是否需要侦听器处理每个事件或仅处理它们获得的最后一个事件。

    【讨论】:

    • 正如我在问题中提到的,事件每 20 毫秒左右发生一次。许多消费者是没有窗口或消息泵的控制台应用程序或服务 - 窗口消息不是一个选项。
    • 是的,但是,消费者不仅需要处理每个事件,还需要在事件发生后的 10 毫秒内完成。这里可能对我来说没有什么好故事;如果 APC 窃取了一个等待线程,似乎我所能做的就是“记录”我错过了一个间隔。
    • 啊,你坚持PulseEvent,伙计。或者只是使用SetEvent/ResetEvent 真的很快,这本质上是一样的。 @codeka 建议可能会帮助您检测错过的事件,但它不会大大提高可靠性,除非侦听器在检测到他们错过了事件时没有“赶上”。
    【解决方案3】:

    PulseEvent 是“不可靠”的原因与其说是因为函数本身有任何问题,不如说是因为如果您的消费者没有碰巧在确切的时刻等待事件> PulseEvent 被调用,它会错过它。

    在您的情况下,我认为最好的解决方案是自己手动保留计数器。所以生产者线程保持对当前“时钟滴答”的计数,当消费者线程启动时,它会读取该计数器的当前值。然后,不使用PulseEvent,而是增加“时钟滴答”计数器并使用SetEvent 唤醒等待滴答的所有 线程。当消费者线程醒来时,它会根据生产者的“时钟滴答”检查它的“时钟滴答”值,它会知道已经过了多少滴答。就在它再次等待事件之前,它可以检查是否发生了另一个滴答声。

    我不确定我是否很好地描述了上述内容,但希望能给你一个想法:)

    【讨论】:

    • 感谢您的回答;我试图代表消费者避免“投票”行为。我会研究一下你的评论。不确定它与使用 PulseEvent 是否有很大不同,然后在给定的消费者醒来时检查我是否“错过了一个”。
    【解决方案4】:

    如果我正确理解您的问题,您似乎可以简单地使用SetEvent。它将释放一个线程。只要确保它是一个自动重置事件。

    如果您需要允许多个线程,您可以使用带有CreateSemaphore 的命名信号量。每次调用 ReleaseSemaphore 都会增加计数。例如,如果计数为 3,并且有 3 个线程在等待它,它们都会运行。

    【讨论】:

    • 似乎与 OP 请求不匹配以向所有等待线程发出信号:“自动重置事件对象的状态保持信号状态,直到释放单个等待线程”
    • @VirtualBox:我不完全确定他是否想要多个线程,但我确实添加了命名信号量选项,这将允许这种情况。
    • 我想释放所有进程中的所有等待线程。通常,多个进程中的每一个都有 1 个线程。生产者不知道应该有多少消费者,并且数量会随着时间而变化。
    【解决方案5】:

    事件更适合于一个进程内的线程之间的通信(未命名事件)。正如您所描述的,您有零个或多个客户需要阅读感兴趣的内容。我知道客户的数量是动态变化的。在这种情况下,最佳选择将是命名管道。

    命名管道为王

    如果您只需要向多个进程发送数据,最好使用命名管道,而不是事件。与自动重置事件不同,您不需要每个客户端进程都有自己的管道。每个命名管道都有一个关联的服务器进程和一个或多个关联的客户端进程(甚至零个)。当有许多客户端时,操作系统会自动为每个客户端创建相同命名管道的许多实例。命名管道的所有实例共享相同的管道名称,但每个实例都有自己的缓冲区和句柄,并为客户端/服务器通信提供单独的管道。实例的使用使多个管道客户端可以同时使用同一个命名管道。任何进程都可以作为一个管道的服务器和另一个管道的客户端,反之亦然,从而使对等通信成为可能。

    如果您将使用命名管道,则在您的场景中根本不需要事件,并且无论进程发生什么数据都将有保证的交付 - 每个进程都可能会出现长时间的延迟(例如通过交换),但数据最终将在没有您特别参与的情况下尽快交付。

    关于事件

    如果您仍然对事件感兴趣 - 自动重置事件为王! ☺

    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 开始使用它们(尽管它们出现在 NT - 3.1 的第一个 32 位版本中)并且它们工作得很好。

    您的进程间方案

    很遗憾,您的情况有点复杂。您在多个进程中有多个线程在等待一个事件,并且您必须确保所有线程实际上都收到了通知。除了为每个消费者创建自己的事件之外,没有其他可靠的方法。因此,您需要拥有与消费者一样多的事件。除此之外,您还需要保留已注册消费者的列表,其中每个消费者都有一个关联的事件名称。因此,要通知所有消费者,您必须在循环中为所有消费者事件执行 SetEvent。这是一种非常快速、可靠且便宜的方式。由于您使用的是跨进程通信,因此消费者必须通过其他进程间通信方式(如 SendMessage)注册和取消注册其事件。例如,当消费者进程在您的主通知程序进程中注册自己时,它会将 SendMessage 发送到您的进程以请求唯一的事件名称。您只需增加计数器并返回类似 Event1、Event2 等的内容,并使用该名称创建事件,因此消费者将打开现有事件。当消费者注销时——它关闭它之前打开的事件句柄,并发送另一个 SendMessage,让你知道你也应该在你这边 CloseHandle 最终释放这个事件对象。如果消费者进程崩溃,您将得到一个虚拟事件,因为您不知道应该执行 CloseHandle,但这应该不是问题 - 事件非常快速且非常便宜,并且几乎没有限制内核对象 - 内核句柄的每个进程限制为 2^24。如果您仍然担心,您可能会相反——客户创建事件,但您打开它们。如果它们无法打开 - 则客户端已崩溃,您只需将其从列表中删除即可。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-08-08
      • 1970-01-01
      • 2021-03-25
      • 1970-01-01
      • 2010-09-07
      • 2011-03-09
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多