跨流程事件 - 可靠地释放所有服务员

时间:2013-05-06 06:35:17

标签: c# .net windows multithreading

我通过ManualResetEvent创建了一个跨进程事件。当此事件确实发生时,应该取消阻止n个不同进程中的n个线程并开始运行以获取新数据。问题是,似乎ManualResetEvent.Set后跟立即重置不会导致所有等待线程被唤醒。那里的文档很模糊

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

  

当发出手动重置事件对象的状态信号时,它仍然存在   发出信号,直到它被ResetEvent明确重置为无信号   功能。任意数量的等待线程或随后的线程   对指定的事件对象开始等待操作,可以释放   当对象的状态发出信号时。

有一种名为PulseEvent的方法似乎正是我所需要的,但遗憾的是它也存在缺陷。

  

等待同步对象的线程可能是暂时的   通过内核模式APC从等待状态中删除,然后返回   APC完成后的等待状态。如果调用PulseEvent   在线程从等待中删除的过程中发生   state,因为PulseEvent发布,线程不会被释放   只有那些在被调用时正在等待的线程。   因此,PulseEvent不可靠,不应该被new使用   应用。相反,使用条件变量。

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

  

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

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

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

目前我已经

ChangeEvent = new EventWaitHandle(false, EventResetMode.ManualReset, counterName + "_Event");

ChangeEvent.Set();
Thread.Sleep(1); // increase odds to release all waiters
ChangeEvent.Reset();

3 个答案:

答案 0 :(得分:5)

处理生产者必须唤醒所有消费者和消费者数量不断变化的情况的一个通用选择是使用移动围栏方法。此选项也需要共享内存IPC区域。该方法有时会导致消费者在没有工作时被唤醒,特别是如果许多流程需要调度和负载很高,但除了无望的超载机器外,它们总会被唤醒。

创建几个手动重置事件,并让生产者维护一个计数器,以便设置下一个事件。除NextToFire事件外,所有事件都保留设置。消费者进程等待NextToFire事件。当制作人希望唤醒所有消费者时,它会重置Next + 1事件并设置当前事件。最终将安排所有消费者,然后等待新的NextToFire活动。结果是只有生产者使用ResetEvent,但消费者总是知道下一个唤醒它们的事件。

所有用户初始化:(伪代码是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);

答案 1 :(得分:2)

从.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函数来实现。

答案 2 :(得分:0)

你写道:“PulseEvent似乎正是我所需要的,但不幸的是它也有缺陷”。这确实是PulseEvent存在缺陷,但我不能同意手动重置事件存在缺陷。它非常可靠。有些情况下您可以使用手动重置事件,并且有些情况下您无法使用它们。它并非一劳永逸。还有很多其他工具,如自动重置事件,管道等。

仅通知线程的最佳方式是,如果您需要定期通知它,但不需要跨进程发送数据,则是自动重置事件。你只需要每个线程都有自己的事件。所以,你有尽可能多的事件和线程。

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

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

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