什么是Win32 PulseEvent()函数的替代品?

时间:2010-04-27 22:57:55

标签: winapi synchronization ipc

Win32 API PulseEvent()函数(kernel32.dll)的文档声明此函数“......不可靠,不应由新应用程序使用。相反,使用条件变量“。但是,条件变量不能跨进程边界使用,如(命名)事件可以。

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

Raymen Chen(The Old New Thing)has a blog post指出了PulseEvent()函数的“根本缺陷”性质,但是我没有看到来自Chen或发布的评论的替代方案。

有人可以推荐一个吗?

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

5 个答案:

答案 0 :(得分:6)

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

我对您的问题的理解是您有一个生产者和一个未知数量的消费者,所有这些消费者都是不同的过程。每个消费者都不会错过任何活动。

我想更多地澄清错过一个事件的含义。

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这样的东西给你一个可靠的组播;这样做的好处是你可以将你的消费者转移到不同的机器上......

答案 1 :(得分:2)

PulseEvent存在两个固有问题:

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

另一种方法是广播一个窗口消息,并让任何监听器都有一个顶级消息 - 只有窗口来监听这个特定的消息。

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

该方法的最大问题是,侦听器对事件的处理将延迟队列获取该消息所花费的时间。

您还可以确保使用手动重置事件(以便唤醒所有等待的线程)并执行SetEvent / ResetEvent一些小的延迟(比如说150毫秒),以便为APC暂时唤醒线程来接收您的活动。

当然,这些替代方法中的任何一种是否适用于您取决于您​​需要多长时间触发一次事件,以及您是否需要听众处理每个事件或者只是最后一个事件。

答案 2 :(得分:2)

PulseEvent“不可靠”的原因并不是因为函数本身有任何错误,只是如果您的消费者没有在确切的时刻等待事件< / em> PulseEvent被调用,它会错过它。

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

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

答案 3 :(得分:0)

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

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

答案 4 :(得分:0)

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

命名管道为王

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

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

关于事件

如果您仍对这些活动感兴趣 - 自动重置活动为王! ☺

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

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

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

因此,请按以下方式创建活动:

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

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

不要&#39;曾经调用ResetEvent(因为它自动重置)或PulseEvent(因为它不可靠和不赞成)。甚至微软也承认不应该使用PulseEvent。见https://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx

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

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

我们从未在我们的应用程序中使用过PulseEvent。关于自动重置事件,我们从Windows NT 3.51开始使用它们(虽然它们出现在NT-3.1的第​​一个32位版本中)并且它们运行良好。

您的进程间方案

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