【问题标题】:Question about Win32 ##Event## synchronization object关于Win32##Event##同步对象的问题
【发布时间】:2010-12-01 07:41:58
【问题描述】:

首先介绍一下应用场景:

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

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

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

这两种方法我都试过了

方法一

  1. 创建手动重置事件;
  2. 当状态发生变化时,先调用SetEvent,然后立即调用ResetEvent。

方法二

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

在测试过程中,这两种方法似乎都运行良好。但我认为它们都不可靠,因为:

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

对于## Method 2 ##,微软声称PulseEvent is unreliable and should not be used

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

【问题讨论】:

  • 方法 1 和 2 在概念上几乎相同,因为调用 PulseEvent(); 几乎就像调用 SetEvent(); ResetEvent();
  • 对您来说更重要的是,当状态发生变化时,应用程序会尽快得到通知,或者应用程序不会错过任何一次状态变化?
  • @Dialectus 不错过更重要。一次更改无法通知应用程序 2 次也很重要。
  • 当某些应用程序崩溃时,服务是否应该自动从情况中恢复?

标签: windows winapi


【解决方案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) 事件,应用程序将不会错过另一个状态更改。而且这里用信号量也没什么用。

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

【讨论】:

  • 你是对的。我通过使用出站命名管道而不是事件来更改我的设计。服务程序初始化一个命名管道,所有其他应用程序连接到这个管道,然后阻塞在管道上的 ReadFile 中。一旦事件发生,服务程序将向管道写入通知,让所有其他应用程序都知道。谢谢!
【解决方案2】:

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

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

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

【讨论】:

  • 谢谢你,迈克尔。第三段的好点!看来我应该让消费者线程继续轮询状态数据。我会尝试重新设计。无论如何,我认为在其他一些项目中应该存在这种1toN通知场景。有可靠的解决方案吗?顺便说一句,我并没有完全理解您在第二段中提到的 2 个事件解决方案。
【解决方案3】:

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

关于命名管道

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

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

自动重置事件(或如何摆脱 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 之前打开它们并在之后关闭它们。如果它们无法打开 - 那么客户端已经崩溃,您只需将其从列表中删除。如果您每秒执行多个通知,则此方法会稍慢一些,但您可以关闭事件,但不是每次都关闭,而是在自上次关闭后经过一定时间后 - 您决定。如果您很少通知,您可以每次打开和关闭。

【讨论】:

    猜你喜欢
    • 2011-02-26
    • 1970-01-01
    • 1970-01-01
    • 2011-05-22
    • 2015-04-29
    • 2015-04-26
    • 1970-01-01
    • 2010-10-01
    • 2011-05-14
    相关资源
    最近更新 更多