Warning: file_get_contents(/data/phpspider/zhask/data//catemap/0/windows/17.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Windows 如何保护进程间生产者-消费者消息传递机制,防止因单边崩溃而损坏?_Windows_Synchronization_Interprocess_Consumer_Producer - Fatal编程技术网

Windows 如何保护进程间生产者-消费者消息传递机制,防止因单边崩溃而损坏?

Windows 如何保护进程间生产者-消费者消息传递机制,防止因单边崩溃而损坏?,windows,synchronization,interprocess,consumer,producer,Windows,Synchronization,Interprocess,Consumer,Producer,我在Windows上为一个生产者和一个消费者在共享内存中实现了一个进程间消息队列 我使用一个命名信号量来计数空插槽,一个命名信号量来计数满插槽,一个命名互斥量来保护共享内存中的数据结构 例如,考虑消费者方面。生产者方面也是如此。 首先它等待完整的信号量,然后(1)它从互斥下的队列中获取消息,然后它向空信号量发送信号(2) 问题是: 如果使用者进程在(1)和(2)之间崩溃,那么该进程可以使用的队列中的插槽数将有效地减少一个。 假设当消费者停机时,生产者可以处理队列中的人。(它可以在等待空信

我在Windows上为一个生产者和一个消费者在共享内存中实现了一个进程间消息队列

我使用一个命名信号量来计数空插槽,一个命名信号量来计数满插槽,一个命名互斥量来保护共享内存中的数据结构

例如,考虑消费者方面。生产者方面也是如此。 首先它等待完整的信号量,然后(1)它从互斥下的队列中获取消息,然后它向空信号量发送信号(2)

问题是:

如果使用者进程在(1)和(2)之间崩溃,那么该进程可以使用的队列中的插槽数将有效地减少一个。 假设当消费者停机时,生产者可以处理队列中的人。(它可以在等待空信号量时指定超时,甚至可以指定0表示不等待)

当使用者重新启动时,它可以继续从队列中读取数据。数据不会溢出,但即使在清空所有满插槽后,生产者也会少使用一个空插槽

在多次这样的重启之后,队列将没有可用的插槽,也无法发送消息

问题:


如何避免或恢复这种情况?

以下是一种简单方法的概述,使用事件而不是信号量:

DWORD increment_offset(DWORD offset)
{
    offset++;
    if (offset == QUEUE_LENGTH*2) offset = 0;
    return offset;
}

void consumer(void)
{
    for (;;)
    {
        DWORD current_write_offset = InterlockedCompareExchange(write_offset, 0, 0);

        if ((current_write_offset != *read_offset + QUEUE_LENGTH) && 
            (current_write_offset + QUEUE_LENGTH != *read_offset))
        {
            // Queue is not full, make sure producer is awake
            SetEvent(signal_producer_event);
        }

        if (*read_offset == current_write_offset)
        {
            // Queue is empty, wait for producer to add a message
            WaitForSingleObject(signal_consumer_event, INFINITE);
            continue;
        }

        MemoryBarrier();
        _ReadWriteBarrier;

        consume((*read_offset) % QUEUE_LENGTH);

        InterlockedExchange(read_offset, increment_offset(*read_offset));
    }
}

void producer(void)
{
    for (;;)
    {
        DWORD current_read_offset = InterlockedCompareExchange(read_offset, 0, 0);

        if (current_read_offset != *write_offset)
        {
            // Queue is not empty, make sure consumer is awake
            SetEvent(signal_consumer_event);
        }

        if ((*write_offset == current_read_offset + QUEUE_LENGTH) ||
            (*write_offset + QUEUE_LENGTH == current_read_offset))
        {
            // Queue is full, wait for consumer to remove a message
            WaitForSingleObject(signal_producer_event, INFINITE);
            continue;
        }

        produce((*write_offset) % QUEUE_LENGTH);

        MemoryBarrier();
        _ReadWriteBarrier;

        InterlockedExchange(write_offset, increment_offset(*write_offset));
    }
}
注:

  • 发布的代码是编译的(给出了适当的声明),但我没有对它进行其他测试

  • read\u offset
    是指向共享内存中的
    DWORD
    的指针,指示应从下一个插槽读取哪个插槽。类似地,
    write\u offset
    指向共享内存中的一个
    DWORD
    ,指示下一个应该写入哪个插槽

  • QUEUE_LENGTH+x
    的偏移量指的是与
    x
    偏移量相同的插槽,以便消除满队列和空队列之间的歧义。这就是为什么
    increment\u offset()
    函数检查
    QUEUE\u LENGTH*2
    而不仅仅是
    QUEUE\u LENGTH
    的原因,也是为什么我们在调用
    consume()
    product()
    函数时采用模的原因。(这种方法的一种替代方法是修改生产者,使其永远不使用最后一个可用的插槽,但这会浪费一个插槽。)

  • signal\u consumer\u event
    signal\u producer\u event
    必须是自动重置事件。请注意,设置已设置的事件是不可操作的

  • 如果队列实际为空,则使用者仅等待其事件;如果队列实际为满,则生产者仅等待其事件

  • 当任一进程被唤醒时,它必须重新检查队列的状态,因为存在可能导致虚假唤醒的竞争条件

  • 因为我使用互锁操作,而且一次只有一个进程在使用任何特定的插槽,所以不需要互斥。我已经加入了内存屏障,以确保生产者写入插槽的更改将被消费者看到。如果您不习惯使用无锁代码,您会发现将显示的算法转换为使用互斥锁是很简单的

  • 请注意,
    interloctedcompareeexchange(指针,0,0)
    看起来有点复杂,但它只是一个线程安全的指针,即读取指针上的值。类似地,
    联锁交换(指针、值)
    *指针=值相同但线程安全。根据编译器和目标体系结构的不同,联锁操作可能不是严格必需的,但性能影响可以忽略不计,因此我建议进行防御性编程

考虑消费者在调用
consume()
函数期间(或之前)崩溃的情况。当使用者重新启动时,它将再次拾取相同的消息,并按正常方式进行处理。就制作人而言,没有发生任何异常情况,只是处理消息的时间比平常长。如果生产者在创建消息时崩溃,则会发生类似情况;重新启动时,生成的第一条消息将覆盖未完成的消息,消费者不会受到影响

显然,如果崩溃发生在生产者或消费者中调用
InterlocatedExchange
之后,但在调用
SetEvent
之前,并且如果队列先前分别为空或满,则此时不会唤醒另一个进程。但是,一旦崩溃的进程重新启动,它就会被唤醒。不能丢失队列中的插槽,进程也不能死锁

我认为简单的多生产商单消费者案例应该是这样的:

void producer(void)
{
    for (;;)
    {
        DWORD current_read_offset = InterlockedCompareExchange(read_offset, 0, 0);

        if (current_read_offset != *write_offset)
        {
            // Queue is not empty, make sure consumer is awake
            SetEvent(signal_consumer_event);
        }

        produce_in_local_cache();

        claim_mutex();

        // read offset may have changed, re-read it
        current_read_offset = InterlockedCompareExchange(read_offset, 0, 0);

        if ((*write_offset == current_read_offset + QUEUE_LENGTH) ||
            (*write_offset + QUEUE_LENGTH == current_read_offset))
        {
            // Queue is full, wait for consumer to remove a message
            WaitForSingleObject(signal_producer_event, INFINITE);
            continue;
        }

        copy_from_local_cache_to_shared_memory((*write_offset) % QUEUE_LENGTH);

        MemoryBarrier();
        _ReadWriteBarrier;

        InterlockedExchange(write_offset, increment_offset(*write_offset));

        release_mutex();
    }
}

如果活动生产者崩溃,互斥锁将被检测为已放弃;您可以将这种情况视为正确释放了互斥锁。如果崩溃的进程达到增加写入偏移量的程度,它添加的条目将照常处理;如果不是,它将被下一个声明互斥的生产者覆盖。在这两种情况下都不需要任何特殊操作。

您最好跟踪共享内存块中队列的状态,并使用事件而不是信号量。我不能给出任何更具体的建议;你对现有算法的描述太模糊了。(队列是环形缓冲区、FIFO还是什么?当消费者崩溃后重新启动时,它如何知道首先从哪个插槽读取?)在共享内存中管理的数据结构的细节在这里并不重要。在本讨论中,假设一个具有两个偏移量的环形缓冲区,一个用于读取,一个用于写入,指向恒定大小的插槽。普罗杜