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

时间:2010-12-01 07:41:59

标签: windows winapi

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

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

所以我用一个命名的Event对象来做同步工作。所有应用程序都在等待此事件发出信号,并且服务应用程序将设置此事件以在该状态更改时发出信号。

我需要保证,当状态发生变化时,每个等待的应用程序都会被释放只会被释放

我尝试过这两种方法

方法1

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

    1. 创建手动重置事件;
    2. 当状态改变时,请调用PulseEvent。
    3. 这两种方法在测试期间似乎都运行良好。但我认为它们都不可靠,因为:

      对于##方法1 ##,在调用ResetEvent函数之前,某些等待的线程可能无法执行。

      对于##方法2 ##,Microsoft已声明PulseEvent is unreliable and should not be used

      这种情况有什么可行的解决方案吗?欢迎任何建议。

3 个答案:

答案 0 :(得分:1)

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

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

但是,您要考虑的一件重要事情是要求“每个等待的应用程序都将被释放”。你的整个应用程序将如何处理即将阻止状态更改事件的线程,但还没有完全实现(所以它实际上并不是一个等待发布的应用程序)?与SetEvent()不能保证在事件发出信号时运行阻止事件的所有线程有什么不同?无论哪种方式,你都有各种各样的竞争条件。

答案 1 :(得分:1)

使用O(1)同步原语无法安全地实现此功能。

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

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

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

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

如果您感觉自己是绿色并且不想浪费资源,则可以使用某种反转信号量而不是B组事件。这可以通过由互斥锁同步的一个共享计数器和一个事件(B')来实现,以通知服务。而不是等待整个B设置,服务等待B',当等待结束时,将计数器设置为N.而不是设置其B(i)事件,每个应用程序应该递减计数器,如果计数器降至零,则最后一个应用程序应该只设置B'。

您无法绕过A系列事件。问题不在于设置A事件,而在于重置。通过重置其A(i)事件,应用程序不会错过另一个状态更改。在这里使用信号量没有帮助。

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

答案 2 :(得分:0)

在这种情况下您可以选择命名管道!

在命名管道上

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

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

自动重置事件(或如何摆脱ResetEvent和Pulse事件)

如果您对这些活动仍然感兴趣,我们就去吧。 ☺

正如您正确指出的那样,根本不应使用PulseEvent。此函数存在于最新的32位版本的Windows NT - 3.1中。也许这是可靠的,但现在不是。

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

正如您已经正确指出的那样,即使Microsoft已经承认不应该使用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上关闭 - 事件对象在其最后一个句柄被关闭时被销毁)。如果消费者进程崩溃,你将最终得到一个虚拟事件,因为你不知道你应该做CloseHandle,但这不应该是一个问题 - 事件非常快且非常便宜,并且几乎没有限制内核对象 - 内核句柄的每进程限制为2 ^ 24。这可能是一个临时解决方案 - 为了确保一切正常,但是您应该更改程序逻辑,以便客户端创建事件,但在调用SetEvent之后打开它们并在之后关闭。如果它们无法打开 - 那么客户端已崩溃,您只需将其从列表中删除即可。如果您每秒执行多个通知,这是一个稍微慢一点的方法,但您可以每次都关闭事件,但只有在上次关闭后经过一定时间后才会关闭 - 您决定。如果您很少通知,则可以每次打开和关闭。