【操作系统】原语之经典问题 - “生产者-消费者问题”

生产者-消费者(producer-consumer)问题,也称作有界缓冲区(bounded-buffer)问题。两个进程共享一个公共的固定大小的缓冲区。其中一个是生产者,将信息放入缓冲区;另一个是消费者,从缓冲区中取出信息。(也可以把这个问题一般化为m个生产者和n个消费者问题,但是我们只讨论一个生产者和一个消费者的情况,这样可以简化解决方案。)

问题在于当缓冲区已满,而此时生产者还想向其中放入一个新的数据项的情况。其解决办法是让生产者睡眠,待消费者从缓冲区中取出一个或多个数据项时再唤醒它。同样地,当消费者试图从缓冲区中取数据而发现缓冲区为空时,消费者就睡眠,直到生产者向其中放入一些数据时再将其唤醒。

这个方法听起来很简单,但它包含与前边假脱机目录问题一样的竞争条件。为了跟踪缓冲区中的数据项数,我们需要一个变量count。如果缓冲区最多存放N个数据项,则生产者代码将首先检查count是否达到N,若是,则生产者睡眠;否则生产者向缓冲区中放入一个数据项并增量count的值。

消费者的代码与此类似:首先测试count是否为0,若是,则睡眠;否则从中取走一个数据项并递减count的值。每个进程同时也检测另一个进程是否应被唤醒,若是则唤醒之。生产者和消费者的代码如图所示。

在这里插入图片描述

为了在C语言中表示sleep和wakeup这样的系统调用,我们将以库函数调用的形式来表示。尽管它们不是标准C库的一部分,但在实际上任何系统中都具有这些库函数。未列出的过程insert_item和remove_item用来记录将数据项放入缓冲区和从缓冲区取出数据等事项。

现在回到竞争条件的问题。这里有可能会出现竞争条件,其原因是对count的访问未加限制。有可能出现以下情况:缓冲区为空,消费者刚刚读取count的值发现它为0。此时调度程序决定暂停消费者并启动运行生产者。生产者向缓冲区中加入一个数据项,count加1。现在count的值变成了1。它推断认为由于count刚才为0,所以消费者此时一定在睡眠,于是生产者调用wakeup来唤醒消费者。

但是,消费者此时在逻辑上并未睡眠,所以wakeup信号丢失。当消费者下次运行时,它将测试先前读到的count值,发现它为0,于是睡眠。生产者迟早会填满整个缓冲区,然后睡眠。这样一来,两个进程都将永远睡眠下去。

问题的实质在于发给一个(尚)未睡眠进程的wakeup信号丢失了。如果它没有丢失,则一切都很正常。一种快速的弥补方法是修改规则,加上一个唤醒等待位。当一个wakeup信号发送给一个清醒的进程信号时,将该位置1。随后,当该进程要睡眠时,如果唤醒等待位为1,则将该位清除,而该进程仍然保持清醒。唤醒等待位实际上就是wakeup信号的一个小仓库。

尽管在这个简单例子中用唤醒等待位的方法解决了问题,但是我们很容易就可以构造出一些例子,其中有三个或更多的进程,这时一个唤醒等待位就不够使用了。于是我们可以再打一个补丁,加入第二个唤醒等待位,甚至是8个、32个等,但原则上讲,这并没有从根本上解决问题。

/// @author zhaolu
/// @date 2020/04/16

#include <iostream> // std::cout std::endl
#include <atomic> // std::atomic_bool
#include <thread> // std::thread std::this_thread

std::atomic_bool flag(false);
int num = 10;

void produce() {
    while (num)
    {
        while (flag != false)
            std::this_thread::yield();
        std::cout << "produce:" << num-- << std::endl;
        flag = true;
    }
}

void consume() {
    while (num)
    {
        while (flag != true)
            std::this_thread::yield();
        std::cout << "consume:" << num + 1 << std::endl;
        flag = false;
    }
}

int main() {
    std::thread producer (produce);
    std::thread consumer (consume);
    producer.join();
    consumer.join();
    return 0;
}

输出结果为:

:g++ -std=c++11 main.cc -o main
:./main
produce:10
consume:10
produce:9
consume:9
produce:8
consume:8
produce:7
consume:7
produce:6
consume:6
produce:5
consume:5
produce:4
consume:4
produce:3
consume:3
produce:2
consume:2
produce:1
consume:1

std::this_thread::yield();
可能这里没写好。。。

发布了447 篇原创文章 · 获赞 14 · 访问量 10万+

猜你喜欢

转载自blog.csdn.net/LU_ZHAO/article/details/105549200