Linux生产者消费者模型

目录

生产者消费者模型

生产者消费者模型的概念

生活中案例类比

生产者消费者模型的特点

生产者消费者模型优点

基于BlockingQueue的生产者消费者模型

基于阻塞队列的生产者消费者模型

模拟实现基于阻塞队列的生产消费模型


生产者消费者模型

生产者消费者模型的概念

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

生活中案例类比

在正式讲解知识之前,可以先引入生活中的实际案例,借此自然地引出知识点,从而帮助大家更深入地理解。

让我们模拟一个超市经营场景,帮助你更直观地理解生产者-消费者模型

场景设定:

你是一家大型超市的老板,本周新引进了一款特别受欢迎的方便面。由于味道极佳,它卖得飞快------周一进货100箱,不到一天就售罄了。

第一天:初始销售

  • 进货:你第一次尝试这款方便面,只进了100箱,放在仓库里。

  • 上架 :你并没有一次性摆满货架,而是先放了5包试卖。

  • 销售过程

    • 顾客小李 来了,看到这款新方便面,买了两包,尝过后觉得非常好吃,于是推荐给了朋友小刚小王

    • 小刚来得早,买走了剩下的3包。

    • 稍后,小王来购买时,发现货架上已经没货了,于是问你:"老板,那款方便面呢?怎么没了?"

    • 你发现5包已经卖完,于是从仓库里又拿出几包补上。

后续运营:动态补货

经过第一天的销售,你发现这款方便面需求旺盛,卖得特别快。于是你调整策略:

  • 联系厂商 :你与供应商达成协议,让他们随时给你送货,确保库存充足。

  • 动态补货 :每当货架上的方便面快卖完(或已经卖完),你就立刻打电话让供应商送货,及时补充货架,避免缺货。


这个超市运营的例子,恰好对应计算机中的生产者-消费者模型

  • 生产者(Producer) = 供应商(负责生产并送货)

  • 消费者(Consumer) = 顾客(购买方便面)

  • 缓冲区(Buffer) = 货架(临时存放商品,供消费者取用)

关键机制:

  1. 库存管理(缓冲区控制)

    • 如果**货架(缓冲区)**空了,消费者(小王)就得等待,直到补货(生产者供货)。

    • 如果货架满了,供应商(生产者)就要停止送货,避免堆积。

  2. 同步与协调

    • 你需要监控库存,确保既不缺货,也不过量堆积。

    • 类似地,在计算机中,线程同步机制(如信号量、锁)确保生产者和消费者协调工作,避免数据竞争或资源浪费。

如此我们也可以从超市来说明缓冲区(仓库)存在的优点:大大方便了生产者与消费者,不需要生产者单独找消费者,也不需要消费者单独找生产者。

生产者消费者模型的特点

上面中共有两个角色:生产者与消费者。

其生产者消费者模型是多线程同步与互斥的一个经典场景,其特点如下:

  • 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
  • 两种角色: 生产者和消费者。(通常由进程或线程承担)。
  • 一个交易场所: 通常指的是内存中的一段缓冲区。(可以自己通过某种方式组织起来)。

我们用代码编写生产者消费者模型的时候,本质就是对这三个特点进行维护。

其中三种关系也不难理解,这里挨个解释,还是非常好理解的。

生产者和生产者(互斥关系)

在之前的超市案例中,我们讨论了生产者(供应商)消费者(顾客) 之间的协作关系。但现实中,生产者之间也需要一定的规则来避免冲突。

问题引入:如果多个供应商同时补货,会发生什么?

假设你的超市生意火爆,为了提高补货效率,你决定同时和两家供应商合作(供应商A和供应商B),让他们都能给你送这款方便面。

  • 情况1(无互斥管理)

    • 早上,货架快空了,你同时通知了供应商A和供应商B补货。

    • 供应商A先到,开始往货架上摆方便面。

    • 还没摆完,供应商B也到了,也开始往同一个货架堆放方便面。

    • 结果:货架上的方便面堆积如山,甚至掉到地上(数据混乱)。

  • 情况2(互斥管理)

    • 你规定:任何时候,只能有一个供应商在补货

    • 当供应商A在补货时,供应商B必须等待,直到A补完并离开。

    • 这样,货架上的方便面始终整齐有序,不会出现混乱。

除此之外,可能还在别的资料里面看到生产者与生产者之间还有竞争,这个也很好理解,超市与不同的方便面供应商合作,供应商之间肯定有竞争。

以此生产者与生产者之间互斥,就避免数据竞争,保证操作完整性,提高系统稳定性。

消费者和消费者(互斥关系)

超市方便面案例解析,场景重现:抢购方便面

假设现在超市货架上只剩最后一包方便面,这时同时来了三个顾客:

  1. 小王伸手去拿

  2. 小李也同时伸手

  3. 小张也看中了这包

如果没有任何规则:

  • 三个人可能同时抢到这包方便面

  • 方便面包装被撕破

  • 最终可能谁都买不成

  • 甚至引发争吵(程序崩溃)

超市的解决方案(互斥机制)

  1. 排队规则(锁机制)

    • 超市规定:每次只能有一个顾客拿取商品

    • 第一个拿到的人就获得购买权

    • 其他人必须等待

  2. 购买流程

    • 小王先拿到方便面 → 购买成功

    • 小李和小张需要等待

    • 等小王完成交易后,下一个人才能购买

以此消费者****与消费者之间互斥,就避免数据竞争,保证操作完整性,提高系统稳定性。

生产者和消费者(互斥关系、同步关系)

场景设定:现代化超市的供需平衡

互斥访问机制:当供应商正在补货时

  • 除了其他供应商必须等待(生产者互斥)

  • 还有顾客不能同时取货(生产者-消费者互斥)

当顾客正在购买时:

  • 除了其他顾客必须等待(消费者互斥)

  • 还有供应商不能同时补货(消费者-生产者互斥)

同步协作机制:货架状态触发不同行为

  • 空货架(需唤醒生产者):顾客线程进入等待,直到补货完成。

  • 满货架(需唤醒消费者):供应商线程进入等待,直到商品被购买。

注意: 互斥关系保证的是数据的正确性,而同步关系是为了让多线程之间协同起来。所以是先保证互斥,才保证同步。

生产者消费者模型优点

  • 解耦。
  • 支持并发。
  • 支持忙闲不均。

这里的解耦,可以从三方面来解释:

时间耦合

  • 高耦合:生产和消费必须同步进行(现做现卖)

  • 解耦:生产可以提前进行,通过库存缓冲

空间耦合

  • 高耦合:同一物理空间完成所有操作(前店后厂)

  • 解耦:生产、存储、销售分离(工厂+物流+超市)

控制耦合

  • 高耦合:直接调用对方方法(收银员直接命令供应商)

  • 解耦:通过消息/事件交互(库存预警触发补货)

其主要是避免了其中一方出问题时,只需要解决该方的问题,不会过多的牵扯到另一方,这也可以从生活经常见,比如说任务分工,如果某人出现问题,在该任务下不会影响别的人对该任务分到的分任务的解决。

因此生产者消费者模型本质是一种松耦合的。

基于BlockingQueue的生产者消费者模型

基于阻塞队列的生产者消费者模型

在多线程编程中,阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。

其与普通的队列的区别在于:

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中放入了元素。
  • 当队列满时,往队列里存放元素的操作会被阻塞,直到有元素从队列中取出。

以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞。

知识联系: 看到以上阻塞队列的描述,我们很容易想到的就是管道,而阻塞队列最典型的应用场景实际上就是管道的实现。管道其就是进程间通信,所以其生产者消费者模型本质也就是通信。

模拟实现基于阻塞队列的生产消费模型

为了方便,下面我们以最简单的单生产者、单消费者为例进行实现。

其中的BlockQueue就是生产者消费者模型当中的交易场所,我们可以用C++STL库当中的queue进行实现。

复制代码
#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>


// 设置一个锁 两个条件变量

template <class T>
class BlockQueue
{
    static const int defalutnum = 20;
public:
    BlockQueue(int maxcap = defalutnum):maxcap_(maxcap)
    {
        pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&c_cond_, nullptr);
        pthread_cond_init(&p_cond_, nullptr);
    }
    bool isFull()
    {
        return q_.size() == maxcap_;
    }
    bool isEmpty()
    {
        return q_.size() == 0;
    }
    void push(const T &in)
    {
        pthread_mutex_lock(&mutex_);
        // 插入的前提是:队列不满
        while(isFull()) // 如果满则等待
        {
            pthread_cond_wait(&p_cond_, &mutex_); // 调用的时候,自动释放锁
        }
        // 不满,插入
        q_.push(in); // 生产完后,要记得唤醒在Empty条件变量下等待的消费者线程
        pthread_cond_signal(&c_cond_);
        pthread_mutex_unlock(&mutex_);

    }
    T pop()
    {
        pthread_mutex_lock(&mutex_);
        // 删除的前提是:队列不为空
        while(isEmpty())
        {
            pthread_cond_wait(&c_cond_, &mutex_);
        }
        T out = q_.front();
        q_.pop();
        pthread_cond_signal(&p_cond_);
        pthread_mutex_unlock(&mutex_);

        return out;
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&c_cond_);
        pthread_cond_destroy(&p_cond_);
    }
private:
    std::queue<T> q_; // 共享资源, q被当做整体使用的,q只有一份,加锁。但是共享资源也可以被看做多份!
    int maxcap_;

    pthread_mutex_t mutex_;
    pthread_cond_t c_cond_; // 消费
    pthread_cond_t p_cond_; // 生产
};

代码部分解释说明:

  • 可以将BlockingQueue当中存储的数据模板化,方便以后需要时进行复用。
  • 这里设置BlockingQueue存储数据的上限为20,当阻塞队列中存储了五组数据时生产者就不能进行生产了,此时生产者就应该被阻塞。
  • 阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来。
  • 插入的前提是:队列不满。若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。
  • 删除的前提是:队列不空。若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。
  • 生产者与消费者两个不同的身份,所以要设置两个不同的条件变量。用于维护生产者与生产者之间的虽然当前为单生产,单消费用不到,但还是要有的。
  • 不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的,但如果条件不满足,那么对应的线程就会被挂起。但如果该线程是拿着锁的,为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。
  • 当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据,而此时可能有消费者线程正在empty条件变量下进行等待,因此当生产者生产完数据后需要唤醒在empty条件变量下等待的消费者线程。
  • 同样的,当消费者消费完一个数据后,意味着阻塞队列当中至少有一个空间,而此时可能有生产者线程正在full条件变量下进行等待,因此当消费者消费完数据后需要唤醒在full条件变量下等待的生产者线程。

判断是否满足生产消费条件时不能用if,而应该用while:

  • pthread_cond_wait作为函数,作为是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。如果插入时失败,明明已经满了,但还往下执行,必定会出现异常。
  • 其次就是当是多消费者模型时,当生产者生产了一个数据后使用pthread_cond_broadcast就会唤醒全部的消费者,但仅仅生产了一个数据,这就导致多消费者伪唤醒,空空浪费资源。
  • 因此为了避免以上的情况,我们就要让其线程被唤醒后再次判断,确认是否真的满足生产消费条件,如果不满足,进入while循环,继续进行调用pthread_cond_wait,反之执行下面的代码。

在主函数中我们就只需要创建一个生产者线程和一个消费者线程,让生产者线程不断生产数据,让消费者线程不断消费数据。

复制代码
#include "BlockQueue.hpp"

void* Producer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//生产者不断进行生产
	while (true){
		sleep(1);
		int data = rand() % 100 + 1;
		bq->push(data); //生产数据
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//消费者不断进行消费
	while (true){
		sleep(1);
		int data = bq->pop(); //消费数据
		std::cout << "Consumer: " << data << std::endl;
	}
}
int main()
{
	srand((int)time(nullptr));
	pthread_t producer, consumer;
	BlockQueue<int>* bq = new BlockQueue<int>;
	//创建生产者线程和消费者线程
	pthread_create(&producer, nullptr, Producer, bq);
	pthread_create(&consumer, nullptr, Consumer, bq);

	//join生产者线程和消费者线程
	pthread_join(producer, nullptr);
	pthread_join(consumer, nullptr);
	delete bq;
	return 0;
}

那么我们让我们的代码运行:

生产者消费者步调一致

可以看到,生产者消费者步调一致,这是因为生产者是每隔一秒生产一个数据,消费者也是每隔一秒消费一个数据,因此我们可以看到生产者消费者步调一致。

注意:前面的打印错乱不用管,其实际也是生产者消费者步调一致,但因为不同线程的原因所以会错乱,根据前面的知识,你知道其原因么?
生产者生产的快,消费者消费的慢

同样我们可以修改上面的代码,使得生产者生产的快,消费者消费的慢,其修改思想就是生产者不停的生产只要满足不满的条件,就生产,但消费者是每隔一秒消费一个数据。

运行效果:

生产者生产的慢,消费者消费的快

当然,我们也可以让生产者每隔一秒进行生产,而消费者不停的进行消费。代码修改原理一样,只将消费者的sleep注释即可。

虽然消费者消费的很快,但一开始阻塞队列中是没有数据的,因此消费者只能在empty条件变量下进行等待,直到生产者生产完一个数据后,消费者才会被唤醒进而进行消费,消费者消费完这一个数据后又会进行等待,因此生产者和消费者的步调就是一致的。

同样我们还可以设定特定条件下才会进行再唤醒对应的生产者或消费者

比如我们规定,当阻塞队列当中存储的数据大于队列容量的一半时,再唤醒消费者线程进行消费;当阻塞队列当中存储的数据小于队列容器的一半时,再唤醒生产者线程进行生产。 一半也就是10。

修改完后的.hpp代码(.hpp后缀的文件就是将.h与.cpp后缀的文件结合一起,也就是将实现与声明结合)

复制代码
#include "BlockQueue.hpp"

void* Producer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//生产者不断进行生产
	while (true){
		sleep(1);
		int data = rand() % 100 + 1;
		bq->push(data); //生产数据
		std::cout << "Producer: " << data << std::endl;
	}
}
void* Consumer(void* arg)
{
	BlockQueue<int>* bq = (BlockQueue<int>*)arg;
	//消费者不断进行消费
	while (true){
		// sleep(1);
		int data = bq->pop(); //消费数据
		std::cout << "Consumer: " << data << std::endl;
	}
}
int main()
{
	srand((int)time(nullptr));
	pthread_t producer, consumer;
	BlockQueue<int>* bq = new BlockQueue<int>;
	//创建生产者线程和消费者线程
	pthread_create(&producer, nullptr, Producer, bq);
	pthread_create(&consumer, nullptr, Consumer, bq);

	//join生产者线程和消费者线程
	pthread_join(producer, nullptr);
	pthread_join(consumer, nullptr);
	delete bq;
	return 0;
}

运行效果: