生产者消费者模型

生产者消费者模型是Linux并发编程中的经典模式,用于解决多线程/进程间数据共享和同步的问题。

在这里我们实现基于BlockingQueue的生产者消费者模型

1 前置知识

为何要使用生产者消费者模型

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

生产者消费者模型优点

• 解耦

• 支持并发

• 支持忙闲不均

核心组件

实现关键

如何正确的进行生产和消费?

本质就是维护生产者和消费者之间的关系(资源有限)

  1. 生产者之间的关系:互斥(生产者需要竞争资源)
  2. 消费者之间的关系:互斥(消费者之间也需要竞争资源)
  3. 生产者和消费者的关系:互斥与同步(生产者和消费者之间不仅需要竞争资源,还需要保证竞争资源的顺序)

换一种说法就是:生产者/消费者需要**互斥访问和同步协调**

  • 互斥访问:多个生产者/消费者不能同时操作缓冲区
  • 同步协调
    • 缓冲区满时,生产者等待
    • 缓冲区空时,消费者等待

简单记忆:"321"原则

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

2种角色:生产者和消费者

一个交易场所:通常是某种数据结构对象,比如阻塞队列(本质是内存块)

2.实践

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

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

C++ queue模拟阻塞队列的生产消费模型

代码:

• 为了便于理解,我们以单生产者,单消费者,来进行讲解。

• 刚开始写,我们采用原始接口。

• 我们先写单生产,单消费。然后改成多生产,多消费(这里代码其实大致不变)。

cpp 复制代码
BlockQueue_test:BlockQueue_test.cpp
	g++ -o $@ $^ -std=c++17 -lpthread
.PHONY:clean
clean:
	rm -f BlockQueue_test

(这里的代码维护了生产者和消费者的互斥与同步关系)

cpp 复制代码
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
using namespace std;
// 📌 注意:这里采用模版,是想告诉我们,队列中不仅仅可以防止内置类型,
// 比如int, 对象也可以作为任务来参与生产消费的过程哦.
template <typename T>
class BlockQueue
{
public:
    BlockQueue(int cap)
        : _capicity(cap)
    {
        pthread_mutex_init(&_mutex, NULL);
        pthread_cond_init(&_c, NULL);
        pthread_cond_init(&_p, NULL);
    }
    void Entry(const T &in)
    {
        pthread_mutex_lock(&_mutex);
        // 要进行生产, 就一定能够进行生产吗?要满足生产条件!!
        // isFull()放在while的判断中是为了预防
        // pthread_cond_wait函数的两种情况
        // 1. 调用失败
        // 2. 伪唤醒情况
        // 确保进行生产是时一定满足生产条件
        while (isFull())
        {
            // 细节1: 我在进行等待的时候可是在临界区里等啊!我可是持有锁的!所以,需要自动让线程 释放锁!!!
            // 细节2: 我为什么把自己弄得需要再临界区内部等?先判断队列是否为满(生产条件是否满足)
            // 判断队列是否为满,本身就是访问临界资源!! 判断队列是否为满,必须在临界区内部判断
            // 生产者必须先申请锁,在临界区内部判断 判断为满的结果,需要等待的结构,也一定在临界区内部!
            // 所以,等待的时候,在内部释放锁是必然的! 所以,锁被做到的pthread_cond_wait的参数中!!!
            _p_wait_num++;
            pthread_cond_wait(&_p, &_mutex); // 特征1:自动释放锁! 特征2: 自动重新竞争直到持有锁
            _p_wait_num--;
            // 当我们被唤醒的时候,就一定又从这个位置唤醒了!
            // 是在临界区内被唤醒的!!!
        }
        _bq.push(in);
        cout << "生产者生产了一个数据:" << in << endl;
        // 此时阻塞队列一定不为空,满足消费者条件
        // 生产者和消费者互相唤醒
        if(_c_wait_num > 0)//有消费者在等待时才唤醒,提高效率
        pthread_cond_signal(&_c);
        pthread_mutex_unlock(&_mutex);
    }
    void Pop(T *out)
    {
        pthread_mutex_lock(&_mutex);
        while (isEmpty())
        {
            _c_wait_num++;
            pthread_cond_wait(&_c, &_mutex);
            _c_wait_num--;
        }
        *out = _bq.front();
        _bq.pop();
        cout << "消费者消费了一个数据:" << *out << endl;
        // 此时阻塞队列一定不为满,满足生产者条件
        // 生产者和消费者互相唤醒
        if(_p_wait_num > 0)//有生产者在等待时才唤醒,提高效率
        pthread_cond_signal(&_p);
        pthread_mutex_unlock(&_mutex);
    }
    bool isFull()
    {
        return _bq.size() == _capicity;
    }
    bool isEmpty()
    {
        return _bq.size() == 0;
    }
    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_c);
        pthread_cond_destroy(&_p);
    }

private:
    queue<T> _bq;
    int _capicity;
    pthread_mutex_t _mutex;
    pthread_cond_t _c; // 消费者的条件变量
    pthread_cond_t _p; // 生产者的条件变量
    int _c_wait_num; // 当前消费者等待的个数
    int _p_wait_num; // 当前生产者等待的个数
};
cpp 复制代码
#include "BlockQueue.hpp"
#include<string>
#include<unistd.h>
#define Capacity 5
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
struct ThreadData
{
    BlockQueue<int> *_bq;
    string name;
};

void *producer(void *arg)
{
    ThreadData *td=(ThreadData*)arg;
    int data=1;
    while(true)
    {
        td->_bq->Entry(data);
        data++;
        sleep(1);
    }
}

void *consumer(void *arg)
{
    ThreadData *td=(ThreadData*)arg;
    int data;
    while(true)
    {
        td->_bq->Pop(&data);
        //sleep(1);
    }
}


int main()
{
    BlockQueue<int> *bq=new BlockQueue<int>(Capacity);
    pthread_t c,p;
    ThreadData ctd={bq,"消费者"};
    ThreadData ptd={bq,"生产者"};
    pthread_create(&p,NULL,producer,(void*)&ptd);
    pthread_create(&c,NULL,consumer,(void*)&ctd);

    pthread_join(p,NULL);
    pthread_join(c,NULL);
    return 0;
}

生产者每生产一个数据就休眠一秒,消费者不休眠一直消费时

生产者一直生产不休眠,消费者每消费一个数据就休眠一秒时

在测试一下多线程(代码不用有太多改动,因为互斥锁天然维护了生产者与生产者,消费者与消费者的互斥关系)

hpp不变

cpp 复制代码
#include "BlockQueue.hpp"
#include<string>
#include<unistd.h>
#define Capacity 5
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
struct ThreadData
{
    BlockQueue<int> *_bq;
    string name;
};

void *producer(void *arg)
{
    ThreadData *td=(ThreadData*)arg;
    int data=1;
    while(true)
    {
        td->_bq->Entry(data,td->name);
        data++;
        sleep(1);
    }
}

void *consumer(void *arg)
{
    ThreadData *td=(ThreadData*)arg;
    int data;
    while(true)
    {
        td->_bq->Pop(&data,td->name);
        //sleep(1);
    }
}


int main()
{
    BlockQueue<int> *bq=new BlockQueue<int>(Capacity);
    pthread_t c[3],p[3];
    ThreadData ctd1={bq,"消费者-1"};
    ThreadData ctd2={bq,"消费者-2"};
    ThreadData ctd3={bq,"消费者-3"};
    ThreadData ptd1={bq,"生产者-1"};
    ThreadData ptd2={bq,"生产者-2"};
    ThreadData ptd3={bq,"生产者-3"};
    pthread_create(&p[0],NULL,producer,(void*)&ptd1);
    pthread_create(&p[1],NULL,producer,(void*)&ptd2);
    pthread_create(&p[2],NULL,producer,(void*)&ptd3);
    pthread_create(&c[0],NULL,consumer,(void*)&ctd1);
    pthread_create(&c[1],NULL,consumer,(void*)&ctd2);
    pthread_create(&c[2],NULL,consumer,(void*)&ctd3);

    pthread_join(p[0],NULL);
    pthread_join(c[0],NULL);
    pthread_join(p[1],NULL);
    pthread_join(c[1],NULL);
    pthread_join(p[2],NULL);
    pthread_join(c[2],NULL);
    return 0;
}

3 补充要点

为什么pthread_cond_wait 需要互斥量?

• 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。

• 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

• 按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:

cpp 复制代码
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) 
{
    pthread_mutex_unlock(&mutex);
    //解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
    pthread_cond_wait(&cond);
    pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);

• 由于解锁和等待不是原子操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他线程获取到互斥量,摒弃条件满足,发送了信号,那么pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个pthread_cond_wait 。所以解锁和等待必须是一个原子操作。

• int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t *mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

条件变量的封装

• 基于上面的基本认识,我们已经知道条件变量如何使用,虽然细节需要后面再来进行解释,但这里可以做一下基本的封装,以备后用.

cpp 复制代码
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>

// 对锁进行封装,可以独立使用
class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

// 采用RAII风格,进行锁管理
class LockGuard
{
public:
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};
cpp 复制代码
#pragma once

#include <iostream>
#include <pthread.h>
#include "Mutex.hpp"

class Cond
{
public:
    Cond()
    {
        pthread_cond_init(&_cond, nullptr);
    }
    void Wait(Mutex &lock)
    {
        int n = pthread_cond_wait(&_cond, lock.Get());
    }
    void NotifyOne()
    {
        int n = pthread_cond_signal(&_cond);
        (void)n;
    }
    void NotifyAll()
    {
        int n = pthread_cond_broadcast(&_cond);
        (void)n;
    }
    ~Cond()
    {
        pthread_cond_destroy(&_cond);
    }
private:
    pthread_cond_t _cond;
};
相关推荐
CIb0la3 小时前
在 ARM CPU 上运行 x86 应用的开源项目:FEX
linux·运维·生活
starvapour3 小时前
Ubuntu部署gitlab频繁出现500的问题
linux·ubuntu·gitlab
所得皆惊喜3 小时前
REDIS04_管道的概念、案列演示、管道总结
redis·缓存
打不了嗝 ᥬ᭄3 小时前
【Linux】多路转接 Select , Poll和Epoll
linux·网络·c++·网络协议·http
jianchwa3 小时前
Linux Kernel PCIe SRIOV机制分析
linux·运维·服务器
9ilk3 小时前
【Linux】--- 五种IO模型
linux·运维·网络
羑悻的小杀马特4 小时前
Stream消息队列+地理空间计算+HyperLogLog去重,SCAN安全遍历+RESP协议全解析,一文把它啃透!
数据库·redis·安全·缓存·空间计算·resp
Jk_Mr4 小时前
Linux-进程状态
linux·操作系统·进程
_F_y4 小时前
Linux:进度条编写
linux