这个进程间生产者消费者的实现是否正确且安全地防止进程崩溃?

时间:2017-09-07 21:34:45

标签: c++ windows producer-consumer interprocess

我正在Windows上的两个进程之间开发一个消息队列。 我想支持多个生产者和一个消费者。 队列不能被其中一个进程的崩溃破坏,也就是说,其他进程不受崩溃的影响,并且当崩溃的进程重新启动时,它可以继续通信(使用新的更新状态)。 p>

假设这些代码段中的事件对象是命名Windows自动重置事件的包装器,而互斥对象是命名Windows互斥锁的包装器(我使用C ++非进程间互斥类型作为占位符)。

这是生产者方面:

void producer()
{
    for (;;)
    {
        // Multiple producers modify _writeOffset so must be given exclusive access

        unique_lock<mutex> excludeProducers(_producerMutex);

        // A snapshot of the readOffset is sufficient because we use _notFullEvent.

        long readOffset = InterlockedCompareExchange(&_readOffset, 0, 0);

        // while is required because _notFullEvent.Wait might return because it was abandoned

        while (IsFull(readOffset, _writeOffset))
        {
            _notFullEvent.Wait(INFINITE);

            readOffset = InterlockedCompareExchange(&_readOffset, 0, 0);
        }

        // use a mutex to protect the resource from the consumer
        {
            unique_lock<mutex> lockResource(_resourceMutex);
            produce(_writeOffset);
        }

        // update the state

        InterlockedExchange(&_writeOffset, IncrementOffset(_writeOffset));
        _notEmptyEvent.Set();
    }
}

同样,这是消费者方面:

void consumer()
{
    for (;;)
    {
        long writeOffset = InterlockedCompareExchange(&_writeOffset, 0, 0);

        while (IsEmpty(_readOffset, writeOffset))
        {
            _notEmptyEvent.Wait(INFINITE);
            writeOffset = InterlockedCompareExchange(&_writeOffset, 0, 0);
        }

        {
            unique_lock<mutex> lockResource(_resourceMutex);
            consume(_readOffset);
        }

        InterlockedExchange(&_readOffset, IncrementOffset(_readOffset));
        _notFullEvent.Set();
    }
}

此实施中是否存在任何竞争条件? 它确实可以根据需要防止崩溃吗?

P.S。如果队列的状态受到保护,则队列满足要求。如果崩溃发生在进程(i)或消耗(i)中,那些插槽的内容可能已损坏,并且将使用其他方法来检测甚至纠正那些插槽的损坏。这些手段超出了这个问题的范围。

2 个答案:

答案 0 :(得分:0)

这种实施确实存在竞争条件。 感谢@VTT指出它。

@VTT写道,如果生产者在_notEmptyEvent.Set()之前就死了;那么消费者可能永远陷入困境。

好吧,也许不是永远,因为当生产者恢复时,它将添加一个项目并再次唤醒消费者。但是这个州确实已经腐败了。例如,如果这发生了QUEUE_SIZE次,生产者将看到队列已满(IsFull()将返回true)并且它将等待。这是一个僵局。

我正在考虑以下解决方案,在生产者方面添加注释代码。应在消费者方面进行类似的补充:

void producer()
{
    for (;;)
    {
        // Multiple producers modify _writeOffset so must be given exclusive access

        unique_lock<mutex> excludeProducers(_producerMutex);

        // A snapshot of the readOffset is sufficient because we use _notFullEvent.

        long readOffset = InterlockedCompareExchange(&_readOffset, 0, 0);

        // ====================== Added begin

        if (!IsEmpty(readOffset, _writeOffset))
        {
            _notEmptyEvent.Set();
        }

        // ======================= end Added

        // while is required because _notFullEvent.Wait might return because it was abandoned

        while (IsFull(readOffset, _writeOffset))

如果队列现在不为空,这将导致生产者在有机会运行时唤醒消费者。 这看起来更像是基于条件变量的解决方案,这可能是我的首选模式,如果不是因为不幸的事实,在Windows上,条件变量没有被命名,因此无法在进程之间共享。

如果此解决方案投票正确,我将使用完整代码编辑原始帖子。

答案 1 :(得分:0)

因此,问题中发布的代码存在一些问题:

  • 如前所述,有一个边际竞争条件;如果队列变满,并且所有活动的生成器在设置_notFullEvent之前崩溃,则代码将会死锁。您的答案通过在循环开始时而不是结束时设置事件来正确解决该问题。

  • 你过度锁定;如果只有其中一个生产者一次生产,那么拥有多个生产者通常没什么意义。这禁止直接写入共享内存,您需要一个本地缓存。 (让多个生产者直接写入共享内存中的不同插槽并非不可能,但这会使得很多更难以实现。)

  • 同样,您通常需要能够同时制作和使用,而您的代码不允许这样做。

以下是我使用单个互斥锁(由消费者和生产者线程共享)和两个自动重置事件对象的方法。

void consumer(void)
{
    claim_mutex();
    for (;;)
    {
        if (!IsFull(*read_offset, *write_offset))
        {
            // Queue is not full, make sure at least one producer is awake
            SetEvent(notFullEvent);
        }

        while (IsEmpty(*read_offset, *write_offset))
        {
            // Queue is empty, wait for producer to add a message
            release_mutex();
            WaitForSingleObject(notEmptyEvent, INFINITE);
            claim_mutex();
        }

        release_mutex();
        consume(*read_offset);
        claim_mutex();

        *read_offset = IncrementOffset(*read_offset);
    }
}

void producer(void)
{
    claim_mutex();
    for (;;)
    {
        if (!IsEmpty(*read_offset, *write_offset))
        {
            // Queue is not empty, make sure consumer is awake
            SetEvent(notEmptyEvent);
        }

        if (!IsFull(*read_offset, *write_offset))
        {
            // Queue is not full, make sure at least one other producer is awake
            SetEvent(notFullEvent);
        }

        release_mutex();
        produce_in_local_cache();
        claim_mutex();

        while (IsFull(*read_offset, *write_offset))
        {
            // Queue is full, wait for consumer to remove a message
            release_mutex();
            WaitForSingleObject(notFullEvent, INFINITE);
            claim_mutex();
        }

        copy_from_local_cache_to_shared_memory(*write_offset);
        *write_offset = IncrementOffset(*write_offset);
    }
}