跨进程事件-可靠地释放所有等待者

本文关键字:释放 等待 进程 事件 | 更新日期: 2023-09-27 18:12:10

我通过ManualResetEvent创建了一个跨进程事件。当此事件发生时,n个不同进程中的n个线程可能会被解除阻塞,并开始运行以获取新数据。问题是,ManualResetEvent。设置后立即重置不会导致所有等待线程唤醒。这里的文档非常模糊

http://msdn.microsoft.com/en-us/library/windows/desktop/ms682396 (v = vs.85) . aspx

当手动重置事件对象的状态被标记时,它将保持不变有信号,直到ResetEvent显式地将其重置为无信号函数。任何数量的等待线程或随后的线程开始等待操作,对于指定的事件对象,可以释放而对象的状态是有信号的

有一个叫做脉冲事件的方法,它似乎做了我所需要的,但不幸的是它也有缺陷。

等待同步对象的线程可以是暂时的由内核模式APC从等待状态移除,然后返回到APC完成后的等待状态。如果调用PulseEvent在线程从等待中移除时发生状态时,线程将不会被释放,因为PulseEvent会释放只有那些在它被调用时正在等待的线程。因此,PulseEvent是不可靠的,不应该被新用户使用应用程序。请使用条件变量。

现在MS建议使用条件变量。

条件变量是启用线程的同步原语等待:等到特定情况发生条件变量是不能跨进程共享的用户模式对象

按照文档,我似乎已经没有足够的运气来可靠地做到这一点。是否有一种简单的方法可以在没有规定限制的情况下使用一个ManualResetEvent来完成相同的事情,或者我是否需要为每个侦听器进程创建一个响应事件来为每个订阅的调用者获取ACK ?在这种情况下,我需要一个小的共享内存来注册订阅进程的pid,但这似乎带来了自己的一组问题。当一个进程崩溃或没有响应时会发生什么?…

给出一些上下文。我有新的状态发布,所有其他进程应该从共享内存位置读取。当同时发生多个更新时,错过一个更新是可以的,但是进程必须至少读取最新的值。我可以用超时投票,但这似乎不是一个正确的解决方案。

当前我的位置是

ChangeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, counterName + "_Event");
ChangeEvent.Set();
Thread.Sleep(1); // increase odds to release all waiters
ChangeEvent.Reset();

跨进程事件-可靠地释放所有等待者

对于生产者必须唤醒所有消费者且消费者数量不断变化的情况,一个通用的选择是使用移动围栏方法。这个选项也需要一个共享内存IPC区域。该方法有时确实会导致在没有工作时唤醒消费者,特别是在需要调度许多进程且负载很高的情况下,但它们总是会唤醒,除非在无可救药地过载的机器上。

创建几个手动重置事件,并让生产者维护将设置的下一个事件的计数器。所有事件都保持设置,除了NextToFire事件。消费者进程等待NextToFire事件。当生产者希望唤醒所有消费者时,它重置Next+1事件并设置当前事件。所有消费者最终都将被安排,然后等待新的NextToFire事件。这样做的效果是,只有生产者使用ResetEvent,而消费者总是知道下一个唤醒它们的是哪个事件。

All Users Init:(伪代码是C/c++,不是c#)

// Create Shared Memory and initialise NextToFire;
pSharedMemory = MapMySharedMemory();
if (First to create memory) pSharedMemory->NextToFire = 0;
HANDLE Array[4];
Array[0] = CreateEvent(NULL, 1, 0, "Event1");
Array[1] = CreateEvent(NULL, 1, 0, "Event2");
Array[2] = CreateEvent(NULL, 1, 0, "Event3");
Array[3] = CreateEvent(NULL, 1, 0, "Event4");

唤醒所有

long CurrentNdx = pSharedMemory->NextToFire;
long NextNdx = (CurrentNdx+1) & 3;
// Reset next event so consumers block
ResetEvent(Array[NextNdx]);
// Flag to consumers new value
long Actual = InterlockedIncrement(&pSharedMemory->NextToFire) & 3;
// Next line needed if multiple producers active.
// Not a perfect solution
if (Actual != NextNdx) ResetEvent(Actual);
// Now wake them all up
SetEvent(CurrentNdx);

消费者等待逻辑

long CurrentNdx = (pSharedMemory->NextToFire) & 3;
WaitForSingleObject(Array[CurrentNdx],  Timeout);

从。net 4.0开始,你可以使用MemoryMappedFile来同步进程内存。在这种情况下,将计数器写入MemoryMappedFile并从工作进程中递减它。如果计数器等于零,则主进程允许重置事件。下面是示例代码:

主要过程

//number of WorkerProcess
int numWorkerProcess = 5;
//Create MemroyMappedFile object and accessor. 4 means int size.
MemoryMappedFile mmf = MemoryMappedFile.CreateNew("test_mmf", 4);
MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
EventWaitHandle ChangeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, counterName + "_Event");
//write counter to MemoryMappedFile
accessor.Write(0, numWorkerProcess);
//.....
ChangeEvent.Set();
//spin wait until all workerProcesses decreament counter
SpinWait.SpinUntil(() => {
    int numLeft = accessor.ReadInt32(0);
    return (numLeft == 0);
});

ChangeEvent.Reset();

WorkerProcess

//Create existed MemoryMappedfile object which created by main process.
MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("test_mmf");
MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor();
//This mutex object is used for decreament counter.
Mutex mutex = new Mutex(false, "test_mutex");
EventWaitHandle ChangeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, "start_Event");
//....
ChangeEvent.WaitOne();
//some job...
//decrement counter with mutex lock. 
mutex.WaitOne();
int count = accessor.ReadInt32(0);
--count;
accessor.Write(0, count);
mutex.ReleaseMutex();
/////////////////////////////////////

如果环境小于。net 4.0,可以使用win32 API中的CreateFileMapping函数实现。

你写道:"PulseEvent似乎正是我需要的,但不幸的是它也有缺陷"。这是真的,脉冲事件是有缺陷的,但我不同意手动重置事件是有缺陷的。它非常可靠。有些情况下您可以使用手动重置事件,有些情况下您不能使用它们。它不是一刀切的。还有很多其他工具,比如自动重置事件、管道等。

如果你需要定期通知一个线程,但又不需要跨进程发送数据,那么通知它的最好方法就是自动重置事件。每个线程只需要自己的事件。所以,你有多少线程就有多少事件。

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

如果你使用命名管道,在你的场景中就不需要事件了,无论进程发生了什么,数据都将有保证的交付——每个进程可能会有很长的延迟(例如通过交换),但数据最终会尽快交付,而不需要你的特殊参与。

所有线程(进程)的一个事件只有在通知只有一次的情况下才OK。在这种情况下,您将需要手动重置事件,而不是自动重置事件。例如,如果您需要通知您的应用程序将很快退出,您可以发出这个常见的手动重置事件的信号。但是,正如我之前所写的,在您的场景中,命名管道是最佳选择。