关于Win32 ## Event ##同步对象的问题

首先让我介绍一下应用场景:

我有一个服务应用程序正在监视某个东西的状态,同时也有多个应用程序等待状态改变。 一旦状态改变,每个应用程序将读取状态值(通过一个命名的FileMap对象)并执行相应的操作,然后等待状态再次改变。

所以我使用了一个名为Event的对象来完成同步工作。 所有的应用程序都在等待这个事件发出信号,服务应用程序会设置这个事件,当这个状态发生变化的时候,这个事件将被发送。

我需要保证,当状态改变时, 每个等待的应用程序将被释放,并且只被释放一次

我已经尝试了这两种方法

方法1

  1. 创build一个手动重置事件;
  2. 当状态改变时,首先调用SetEvent,然后立即调用ResetEvent。

方法2

  1. 创build一个手动重置事件;
  2. 当状态改变时,调用PulseEvent。

这两种方法在testing过程中似乎都很好。 但我认为他们都不可靠,因为:

对于##方法1 ##,可能有些等待的线程在调用ResetEvent函数之前不会得到执行的机会。

对于##方法2 ##,微软声称PulseEvent是不可靠的,不应该使用 。

这个案子有没有可行的解决办法? 任何build议是受欢迎的。

O(1)同步原语无法安全地实现。

要通知N个应用程序有关新状态更改,请使用N个事件 服务应该全部设置,并且每个应用程序在处理当前状态改变时应该重置其相应的事件。

要等到所有应用程序处理新的状态更改服务都可以使用另一组N个事件,并使用bWaitAll == TRUE的WaitForMultipleObjects。 每个应用程序应该设置相应的事件。

所以,服务做一个循环:观察状态变化,写入共享内存,设置所有A事件,等待所有B事件,重置所有B事件,继续循环。 每个应用程序执行循环:等待其A(i)事件,重置A(i),处理状态更改,设置B(i),继续循环。

A和B事件都可以是自动重置类型。 那么你不必重置任何东西。

如果你觉得绿色,不想浪费资源,你可以使用某种反向信号而不是B事件。 这可以通过互斥体同步的一个共享计数器和一个事件(B')来实现以通知服务。 而不是等待整个B集的服务等待B',当等待结束时将计数器设置为N.而不是设置其B(i)事件,每个应用程序应递减计数器,如果计数器下降到零,最后的应用程序应该只设置B'。

你不能绕过一组事件。 问题不在于设置A事件,而是在重置。 通过重置其A(i)事件,应用程序不会错过另一个状态更改。 在这里使用信号量是没有用的。

请注意,此解决方案不考虑应用程序可能的崩溃。 如果发生这种情况,服务将永远等待不存在的应用程序作出响应。

方法#1的一个问题(即使不太可能,按时间顺序)是,你不能保证应用程序“只被释放一次” – 有可能等待事件的线程被释放当你调用SetEvent() ,做它的工作,然后尝试在你的线程调用ResetEvent()之前再次等待事件。 我ResetEvent()还没有发生,线程将不会阻塞(基本上被释放不止一次)。

您可能需要第二个处于非信号状态的事件对象,以便线程在“真实”事件对象上发生的SetEvent()/ ResetEvent()序列期间等待。 从本质上讲,每个消费者线程需要等待两个事件的顺序,只有一个事件在任何时刻发出。 通常只有其中一个事件处于线程阻塞的重置状态,但是在main事件对象的SetEvent()/ ResetEvent()序列中将会有一个短暂的时间段,这两个事件都将是一个重置状态。

不过,还是要考虑一件重要的事情,那就是“每个等待的应用程序都会被释放”。 你的整个应用程序将如何处理一个线程,而这个线程正好阻塞状态改变的事件,但是还没有到达那里(所以它实际上不是一个等待被释放的应用程序)? 如果SetEvent()不能保证在事件发送信号的时候所有被事件阻塞的线程都将被运行,那么情况如何呢? 你有任何方式的种族条件。

命名管道是你在这种情况下选择!

在命名的管道上

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

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

自动重置事件(或如何摆脱重置事件和脉冲事件)

如果你仍然对事件感兴趣,那么我们走吧。 ☺

正如您已经正确指出的,PulseEvent不应该被使用。 这个功能是从Windows NT-3.1的第​​一个32位版本开始的。 也许这是可靠的,但不是现在。

请考虑使用命名的自动重置事件。 只是为了重申自动重置事件以及如何使用它们:CreateEvent函数具有bManualReset参数(如果此参数为TRUE,则该函数将创建一个手动重置事件对象,该对象需要使用ResetEvent函数来设置事件状态为非信号 – 这不是你所需要的)。 如果此参数为FALSE,则该函数会创建一个自动重置事件对象,系统会在释放单个等待线程后自动将事件状态重置为无信号,即已从WaitForMultipleObjects或WaitForSigleObject等函数中退出。 使用自动重置事件,您不会调用ResetEvent(因为它自动重置)或PulseEvent(因为它不可靠和不赞成使用)。

正如你也正确地指出,即使微软已经承认PulseEvent不应该使用 – 请参阅https://msdn.microsoft.com/en-us/library/windows/desktop/ms684914(v=vs.85).aspx – 只有那些在调用PulseEvent时处于“等待”状态的线程才会被通知。 如果他们在任何其他状态,他们将不会被通知,你可能永远不知道什么线程状态。 等待同步对象的线程可以通过内核模式的异步过程调用暂时从等待状态中移除,然后在APC完成后返回到等待状态。 如果在线程从等待状态中移除期间发生对PulseEvent的调用,线程将不会被释放,因为PulseEvent只释放那些被调用时正在等待的线程。 您可以在以下链接中找到有关内核模式异步过程调用(APC)的更多信息:

我们从来没有在我们的应用程序中使用过PulseEvent – 我们设法做所有事情,只是自动重置事件 – 自从Windows NT 3.51以来,我们使用它们,它们工作得很好,我们对它们感到非常满意。

如何通知等待申请

您需要通知多个进程中有多个线程。 他们正在等待一个事件,你必须确保所有的线程确实收到通知,并且只接收一次。 除了(惊喜!)使用自动重置事件,没有其他可靠的方法可以做到这一点。 只需为每个等待的应用程序创建自己的命名事件 所以,你需要和等待的应用程序一样多的事件。 除此之外,您需要保留一个注册用户列表,每个用户都有一个关联的事件名称。 所以,要通知所有的消费者,你必须在所有消费者事件的循环中做SetEvent。 这是一个非常快速,可靠和廉价的方法。 由于您正在使用跨进程通信,所以正在等待的应用程序将不得不通过其他方式的进程间通信(如SendMessage)或更复杂的方式将其事件注册并注销为FileMap对象。 为了简单起见,让我举一个使用SendMessage的例子。 当等待的应用程序在主通知程序进程中注册自己时,它将SendMessage发送到您的进程以请求唯一的事件名称。 您只需增加计数器并返回类似于YourAppNameEvent1,YourAppNameEvent2等的内容。在返回此名称之前,请调用CreateEvent实际并创建具有该名称的自动重置命名事件,以便等待的应用程序将使用该名称调用OpenEvent以打开现有事件并获取该事件对象的事件句柄。 当等待的应用程序取消注册时,它将关闭它所打开的事件句柄,并发送另一个SendMessage,让你知道你应该关闭并最终释放这个事件对象(在最后的CloseHandle上关闭 – 事件对象在最后一个句柄关闭时被销毁)。 如果消费者进程崩溃,那么最终会出现一个虚拟事件,因为您不知道应该执行CloseHandle,但这不应该成为一个问题 – 事件非常快速且非常便宜,并且实际上没有任何限制内核对象 – 内核句柄上的每进程限制是2 ^ 24。 这可能是一个临时解决方案 – 为了确保一切正常,但是您应该更改程序逻辑,以便客户端创建事件,但在调用SetEvent并在事后关闭之前打开它们。 如果他们不能打开 – 那么客户端崩溃了,你只是从列表中删除它。 如果每秒执行多个通知,则这是一个稍微慢一些的方法,但是您可以每次都不关闭事件,但是只有在自上次关闭以来已经过去了一段时间之后才能关闭 – 您决定。 如果你很少通知,你可以每次打开和关闭。