Linux生产者消费者模型

1.⽣产者消费者模型

1-1为何要使⽤⽣产者消费者模型

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

⽣产者和消费者彼此之间

不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。

生活中,哪些场景是符合生产和消费的?

超市

为什么要有 "超市"?(生产者 - 消费者模型的意义)

没有超市 = 生产和消费强耦合、强同步

1.买一根肠 → 工厂立刻开机 → 现场做 → 现场给你

2.消费者去工厂买离市区远
效率极低、资源浪费。

有超市 = 解耦

生产者只管提前生产,扔到队列里;

消费者只管从队列里拿,不用等生产线;
两边互不打扰,各自按自己节奏跑。

这就是生产者 - 消费者模型最核心的价值:解耦

总结

1.减少生产和消费的成本

2.支持生产和消费的忙闲不均,超市(中间缓冲区)

3.维护松耦合关系,解耦

1-2 ⽣产者消费者模型优点

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

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

本质就是维护消费者和生产者之间的关系

a.生产者之间是什么关系? (同行之间都想垄断)

互斥

b.消费者之间是什么关系? (都想获取资源)

互斥

c.生产者和消费者之间什么关系?

1.互斥

介于生产者和消费者之间的容器可能会被多个执行流同时访问,因此我们需要将该临界资源用互斥锁保护起来。

其中,所有的生产者和消费者都会竞争式的申请锁,因此生产者和生产者、消费者和消费者、生产者和消费者之间都存在互斥关系。

2.同步

我们应该让生产者和消费者访问该容器时具有一定的顺序性,比如让生产者先生产,然后再让消费者进行消费。

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

生产消费 "串行" 指的是什么?

访问队列、修改队列的那一小段操作是串行、互斥的

同一时间只能一个人动队列。

那为什么效率还高?

真正耗时的业务处理过程是并发的。

生产者在生产数据

消费者在处理数据

这两段可以同时跑、互不等待

所以整体效率极高。

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

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

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

看到以上阻塞队列的描述,我们很容易想到的就是管道,而阻塞队列最典型的应用场景实际上就是管道的实现。

2-2C++ queue模拟阻塞队列的⽣产消费模型

代码:

•我们以单⽣产者,单消费者,来进⾏讲解。

cpp 复制代码
#include<iostream>
#include<string>
#include<queue>
#include<pthread.h>

template<typename T>
class BlockQueue
{
private:
    // 判断队列是否满
    bool IsFull()
    {
        return _block_queue.size() == _cap;
    }

    // 判断队列是否空
    bool IsEmpty()
    {
        return _block_queue.empty();
    }

public:
    BlockQueue(int cap):_cap(cap)
    {
        // 初始化锁 + 条件变量
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_product_cond, nullptr);
        pthread_cond_init(&_consum_cond, nullptr);
    }

    // 生产者:往队列放数据
    void Enqueue(T &in)
    {
        pthread_mutex_lock(&_mutex);

        // 满了就等待(必须while!)
        while(IsFull())
        {
            pthread_cond_wait(&_product_cond, &_mutex);
        }

        // 生产入队
        _block_queue.push(in);

        // 唤醒一个消费者
        pthread_cond_signal(&_consum_cond);

        pthread_mutex_unlock(&_mutex);
    }

    // 消费者:从队列拿数据
    void Pop(T *out)
    {
        pthread_mutex_lock(&_mutex);

        // 空了就等待(必须while!)
        while(IsEmpty())
        {
            pthread_cond_wait(&_consum_cond, &_mutex);
        }

        // 消费
        *out = _block_queue.front();
        _block_queue.pop();

        // 唤醒一个生产者
        pthread_cond_signal(&_product_cond);

        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_product_cond);
        pthread_cond_destroy(&_consum_cond);
    }

private:
    std::queue<T> _block_queue;
    int _cap;

    pthread_mutex_t _mutex;
    pthread_cond_t _product_cond;  // 生产者等待
    pthread_cond_t _consum_cond;   // 消费者等待
};

•阻塞队列是会被生产者和消费者同时访问的临界资源,因此我们需要用一把互斥锁将其保护起来。

•生产者线程要向阻塞队列当中Push数据,前提是阻塞队列里面有空间,若阻塞队列已经满了,那么此时该生产者线程就需要进行等待,直到阻塞队列中有空间时再将其唤醒。

•消费者线程要从阻塞队列当中Pop数据,前提是阻塞队列里面有数据,若阻塞队列为空,那么此时该消费者线程就需要进行等待,直到阻塞队列中有新的数据时再将其唤醒。

•因此在这里我们需要用到两个条件变量,一个条件变量用来描述队列为空,另一个条件变量用来描述队列已满。当阻塞队列满了的时候,要进行生产的生产者线程就应该在_product_cond条件变量下进行等待;当阻塞队列为空的时候,要进行消费的消费者线程就应该在_consum_cond条件变量下进行等待。

•不论是生产者线程还是消费者线程,它们都是先申请到锁进入临界区后再判断是否满足生产或消费条件的,如果对应条件不满足,那么对应线程就会被挂起。但此时该线程是拿着锁的,为了避免死锁问题,在调用pthread_cond_wait函数时就需要传入当前线程手中的互斥锁,此时当该线程被挂起时就会自动释放手中的互斥锁,而当该线程被唤醒时又会自动获取到该互斥锁。

•当生产者生产完一个数据后,意味着阻塞队列当中至少有一个数据,而此时可能有消费者线程正在empty条件变量下进行等待,因此当生产者生产完数据后需要唤醒在empty条件变量下等待的消费者线程。

•同样的,当消费者消费完一个数据后,意味着阻塞队列当中至少有一个空间,而此时可能有生产者线程正在full条件变量下进行等待,因此当消费者消费完数据后需要唤醒在full条件变量下等待的生产者线程。

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

•pthread_cond_wait函数是让当前执行流进行等待的函数,是函数就意味着有可能调用失败,调用失败后该执行流就会继续往后执行。

•其次,在多消费者的情况下,当生产者生产了一个数据后如果使用pthread_cond_broadcast函数唤醒消费者,就会一次性唤醒多个消费者,但待消费的数据只有一个,此时其他消费者就被伪唤醒了。

eg:

队列容量 = 1

现在队列是空的,三个消费者都在 wait

生产者放了 1 个数据

调用 broadcast,把 3 个消费者全部喊醒

这 3 个开始疯狂抢锁

第一步

线程 A 抢到锁,直接消费

消费完,队列又空了

线程 A 解锁

第二步

线程 B 终于抢到锁

→ 但它醒来的时候,队列已经空了!

第三步

线程 C 抢到锁

→ 队列也是空的!

broadcast 只是喊大家起来看看,不是保证大家都能干活,抢到锁后,条件可能已经变了

所以必须 while 再判断一次,满足才消费,不满足继续睡

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

cpp 复制代码
#include "BlockQueue.hpp"
BlockQueue<int> g_bq(5);

void* producer(void* arg)
{
    int data = 0;
    while(true)
    {
        usleep(1000);
        g_bq.Enqueue(data);
        printf("生产者生产: %d\n",data);
        data++;
    }
     return nullptr;
}

void* consumer(void* arg)
{
    int data;
    while(true)
    {
      g_bq.Pop(&data);
      printf("消费者消费: %d\n",data);
      usleep(1000);
    }
    return nullptr;
}

int main()
{
    pthread_t pro,con;

    pthread_create(&pro,nullptr,producer,nullptr);
    pthread_create(&con,nullptr,consumer,nullptr);

    pthread_join(pro,nullptr);
    pthread_join(con,nullptr);

    return 0;
}

由于代码中生产者是每隔1ms生产一个数据,而消费者是每隔1ms消费一个数据,因此运行代码后我们可以看到生产者和消费者的执行步调是一致的。

当然,实际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,我们这样做只是为了测试代码的正确性。

由于我们将BlockingQueue当中存储的数据进行了模板化,此时就可以让BlockingQueue当中存储其他类型的数据。

例如,我们想要实现一个基于计算任务的生产者消费者模型,此时我们只需要定义一个Task类,这个类当中需要包含一个Run成员函数,该函数代表着我们想让消费者如何处理拿到的数据。

cpp 复制代码
#include "BlockQueue.hpp"
using namespace std;
class Task
{
public:
    // 构造函数:传入要计算的两个数 + 操作符
    Task(int x, int y, char op) : _x(x), _y(y), _op(op), _res(0), _exitCode(0)
    {}

    // 核心:Run 方法 ------ 消费者真正要做的事
    void Run()
    {
        switch(_op)
        {
            case '+':
                _res = _x + _y;
                break;
            case '-':
                _res = _x - _y;
                break;
            case '*':
                _res = _x * _y;
                break;
            case '/':
                if(_y == 0)
                {
                    _exitCode = -1; // 除0错误
                }
                else
                {
                    _res = _x / _y;
                }
                break;
            case '%':
                if(_y == 0)
                {
                    _exitCode = -2;
                }
                else
                {
                    _res = _x % _y;
                }
                break;
            default:
                break;
        }
    }

    // 打印任务结果
    void ShowResult()
    {
        cout << _x << _op << _y << " = " << _res << " (exitCode: " << _exitCode << ")" << endl;
    }

private:
    int _x, _y;
    char _op;
    int _res;
    int _exitCode;
};

// ===================== 全局阻塞队列:存储 Task 任务 =====================
BlockQueue<Task> g_bq(5);

// ===================== 生产者:生产任务 =====================
void* producer(void* arg)
{
    const char* op = "+-*/%";
    while(true)
    {
        // 1. 生成随机计算任务
        int x = rand() % 100;
        int y = rand() % 100;
        char oper = op[rand() % 5];

        // 2. 封装成任务
        Task t(x, y, oper);

        // 3. 放入队列
        g_bq.Enqueue(t);
        cout << "生产者生产任务:" << x << oper << y << endl;

        usleep(100000);
    }
    return nullptr;
}

// ===================== 消费者:执行任务 =====================
void* consumer(void* arg)
{
    while(true)
    {
        // 1. 从队列拿任务
        Task t(0, 0, ' ');
        g_bq.Pop(&t);

        // 2. 执行任务!!!
        t.Run();

        // 3. 打印结果
        cout << "消费者执行任务:";
        t.ShowResult();

        usleep(150000);
    }
    return nullptr;
}

// ===================== 主函数 =====================
int main()
{
    srand((unsigned int)time(nullptr));

    pthread_t pro, con;
    pthread_create(&pro, nullptr, producer, nullptr);
    pthread_create(&con, nullptr, consumer, nullptr);

    pthread_join(pro, nullptr);
    pthread_join(con, nullptr);

    return 0;
}

也就是说,此后我们想让生产者消费者模型处理某一种任务时,就只需要提供对应的Task类,然后让该Task类提供一个对应的Run成员函数告诉我们应该如何处理这个任务即可。

上一篇有锁的封装
条件变量的封装

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
#include "Lock.hpp"
namespace CondModule
{
 using namespace LockModule;
 
 class Cond
 {
 public:
 Cond()
 {
 int n = pthread_cond_init(&_cond, nullptr);
 (void)n; // 酌情加⽇志,加判断 
 }
 void Wait(Mutex &mutex)
 {
 int n = pthread_cond_wait(&_cond, mutex.GetMutexOriginal());
 (void)n;
 }
 void Notify()
 {
 int n = pthread_cond_signal(&_cond);
 (void)n;
 }
 void NotifyAll()
 {
 int n = pthread_cond_broadcast(&_cond);
 (void)n;
 }
 ~Cond()
 {
 int n = pthread_cond_destroy(&_cond);
 (void)n; // 酌情加⽇志,加判断 
 }
 private:
 pthread_cond_t _cond;
 };
}
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言