『Linux』第九讲:Linux多线程详解(四)_ 生产者消费者模型

news2024/11/27 12:31:49

「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(三),今天这篇是 Linux多线程详解(四),内容大致是生产消费者模型,讲解下面开始!

「归属专栏」Linux系统编程

「笔者」枫叶先生(fy)

「座右铭」前行路上修真我

「枫叶先生有点文青病」

「每篇一句」

 记住该记住的,忘记该忘记的。

改变能改变的,接受不能改变的。

——塞林格《麦田守望者》

目录

八、生产者消费者模型

8.1 概念

8.2 生产者消费者模型的特点

8.3 生产者消费者模型优点

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

9.1 概念

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


八、生产者消费者模型

8.1 概念

生产者消费者模型是指在一个系统中,存在生产者和消费者两种角色生产者负责生产数据,消费者负责消费数据,二者之间通过共享缓冲区进行通信,以实现数据的传输和处理。生产者生产数据后将数据放入缓冲区中,而消费者从缓冲区中取出数据进行处理。

这就类似于学校里面的小卖部:

  1. 学生是消费者,
  2. 小卖部是共享的缓冲区,称为交易场所,
  3. 提供产品给小卖部的供货商是生产者,
  4. 这里的产品就是数据

生产者生产产品,生产者把产品放入小卖部,最后学生进行消费产品。

  • 明显发现,生产者与消费者互不影响,生产者只负责生产数据,消费者只负责消费数据,用计算机的术语来说就是:把生产者的生产过程与消费者的消费过程进行解耦
  • 小卖部是交易场所,是一个缓冲区,这个缓冲区的存在可以提供一定量的产品供学生进行一段时间的消费。
  • 消费者疯狂消费产品,生产者此时不生产,等小卖部的产品下降到了一定的水平线后,小卖部再通知生产者进行生产一定量的产品(而不是生产一个产品就消费一个产品)。这个过程说明消费者和生产者的步调可以不一致,这样可以同时提高生产者和消费者的效率

上面有小卖部,可以对生产者和消费者进行解耦,那下面就谈谈生产者和消费者强耦合的情况(也就是没有小卖部,即没有缓冲区的情况)

因为没有小卖部,消费者想要消费数据就只能去跟生产者说,消费者说:我要一根火腿肠,供货商你生产一下。这个过程就势必会让消费者进行等待,这就是生产者和消费者强耦合的情况,这样会导致生产者和消费者的效率大大降低

比如我们平时main主函数中调用某一函数,那么我们必须等该函数体执行完后才继续执行主函数的后续代码,因此函数调用本质上是一种强耦合的关系。

对生产者消费者模型的角色之间的关系分析

  • 一个消费者或多个消费者对应:一个线程或多个线程
  • 一个生产者或多个生产者对应:也是一个线程或多个线程
  • 小卖部对应:共享资源

角色之间的关系分析:

  • 生产者与生产者之间:互斥关系(竞争生产产品)
  • 消费者与消费者之间:互斥关系(共同竞争产品,即共同竞争数据) 
  • 生产者与消费者之间:互斥 && 同步(互斥:生产者生产数据未完成,消费者这时来消费数据;同步:有数据消费者才进行消费,没有数据降到一定水平线生产者才开始生产数据)

生产者和消费者为什么存在同步和互斥的关系?

  • 同步关系:
  • 如果生产者一直生产数据,消费者不消费数据,当生产者生产的数据将缓冲区塞满后,生产者再生产数据就会生产失败
  •  如果消费者一直消费数据,生产者不生产数据,当消费者把缓冲区的数据消费完之后,生消费者再进行消费就会消费失败
  • 虽然这样不会造成任何数据不一致的问题,但是这样会引起另一方的饥饿问题,是非常低效的
  • 所以,生产者和消费者需要存在同步的关系,生产者生产数据达到一定水平线后就不生产,等待消费者进行消费数据,缓冲区的数据下降到一定的水平线生产者就开始生产数据,这就是同步
  • 互斥关系:
  • 生产者生产数据未完成,消费者这时来消费数据,这是就会导致数据不一致的问题
  • 所以,生产者和消费者需要存在互斥的关系,消费者必须等到生产者生产完数据,消费者才可以进行消费(保证共享资源的安全性)

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

8.2 生产者消费者模型的特点

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

321原则(便于记忆)

  • 三种关系: 生产者和生产者(互斥关系)、消费者和消费者(互斥关系)、生产者和消费者(互斥关系、同步关系)。
  • 两种角色: 生产者和消费者(通常由线程承担)
  • 一个交易场所: 通常指的是内存中的一段缓冲区(共享资源)

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

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

8.3 生产者消费者模型优点

  • 解耦(生产者与消费者进行解耦)
  • 支持并发(高效)
  • 支持忙闲不均(忙闲不均,比如生产者生产快,消费者消费慢;消费者消费快,生产者生产慢等)

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

9.1 概念

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

 

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

  • 当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;
  • 当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出
  • (以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

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

为了便于理解,以单生产者,单消费者,来进行讲解,其中阻塞队列就是缓冲区

BlockQueue.hpp

#pragma once

#include <iostream>
#include <thread>
#include <queue>

const int gmaxcap = 5;

template <class T>
class BlockQueue
{
    // public:
    // 如果一个类是模板类,那么它的静态成员变量应该在类的外部进行定义,而不能在类的内部进行定义和初始化。
    // 这是因为模板类在编译时并不会被实例化,只有在使用时才会进行实例化,因此编译器无法确定静态成员变量的类型和值。
    // static const int gmaxcap = 5;

public:
    BlockQueue(const int &maxcap = gmaxcap) : _maxcap(maxcap)
    {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_pcond, nullptr);
        pthread_cond_init(&_ccond, nullptr);
    }

    void push(const T &in) // 输入型参数,一般设置成 const &
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断,不能使用if
        while (is_full())
        {
            // pthread_cond_wait这个函数的第二个参数,必须是我们正在使用的互斥锁!
            // a. pthread_cond_wait: 该函数调用的时候,会以原子性的方式,将锁释放,并将自己挂起
            // b. pthread_cond_wait: 该函数在被唤醒返回的时候,会自动的重新获取你传入的锁
            pthread_cond_wait(&_pcond, &_mutex); // 队列满,不满足生产条件,去生产者的条件变量下等待
        }
        // 2.走到这里的代表队列不为满,该线程一定可以生产数据
        _q.push(in); // 生产数据
        // 3.走到这里,一定能保证阻塞队列里面有数据,此时可以唤醒一个消费者线程去消费数据
        // pthread_cond_signal(&_ccond);//唤醒函数可以放在临界区内部,也可以放在临界区外面
        pthread_mutex_unlock(&_mutex);
        pthread_cond_signal(&_ccond);
    }

    void pop(T *out) // 输出型参数,一般设置成 * ;如果是输入输出型:&
    {
        pthread_mutex_lock(&_mutex);
        // 1.判断,不能使用if
        while (is_empty())
        {
            pthread_cond_wait(&_ccond, &_mutex); // 队列为空,不满足消费条件,去消费者的条件变量下等待
        }
        // 2.走到这里的代表队列不为空,该线程一定可以消费数据
        *out = _q.front();
        _q.pop();
        // 3.走到这里,一定能保证阻塞队列里面有一个空的位置,此时可以唤醒一个生产者线程去生产数据
        pthread_cond_signal(&_pcond);
        pthread_mutex_unlock(&_mutex);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_pcond);
        pthread_cond_destroy(&_ccond);
    }

private:
    bool is_empty() // 判断队列是否为空
    {
        return _q.empty();
    }
    bool is_full() // 判断队列是否满
    {
        return _q.size() == _maxcap;
    }

private:
    std::queue<T> _q;
    int _maxcap; // 队列元素的上限
    pthread_mutex_t _mutex;
    pthread_cond_t _pcond; // 生产者对应的条件变量
    pthread_cond_t _ccond; // 消费者对应的条件变量
};

注:由于我们实现的是单生产者、单消费者的生产者消费者模型,因此我们不需要维护生产者和生产者之间的关系,也不需要维护消费者和消费者之间的关系,我们只需要维护生产者和消费者之间的同步与互斥关系即可

主函数:Main.cc

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

#include "BlockQueue.hpp"
#include <unistd.h>
#include <ctime>

void *consumer(void *_bq)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(_bq);
    while (true)
    {
        // 消费活动
        int data;
        bq->pop(&data);
        std::cout << "消费数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *_bq)
{
    BlockQueue<int> *bq = static_cast<BlockQueue<int> *>(_bq);
    while (true)
    {
        // 生产活动
        int data = rand() % 10 + 1;
        bq->push(data);
        std::cout << "生产数据:" << data << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ 0x1122334);

    // 阻塞队列:共享资源
    BlockQueue<int> *bq = new BlockQueue<int>();

    // c:consumer p:productor
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    // 线程等待
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    return 0;
}

 编译运行(生产者消费者步调一致,都是sleep了1秒)

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

修改一下代码

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

运行结果

 

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

修改代码 

此时由于生产者生产的很快,运行代码后一瞬间生产者就将阻塞队列打满了,此时生产者想要再进行生产就只能在条件变量下进行等待,直到消费者消费完一个数据后,生产者才会被唤醒进而继续进行生产,生产者生产完一个数据后又会进行等待,因此后续生产者和消费者的步调又变成一致的了

运行结果

 

 执行计算任务生产者消费者模型

际使用生产者消费者模型时可不是简单的让生产者生产一个数字让消费者进行打印而已,这里只是为了方便测试 

Task.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <functional>

class Task
{
    // 两者用法相同,写法不同
    // typedef std::function<int(int,int)> func_t;
    using func_t = std::function<int(int, int, char)>;
   
public:
    Task()
    {}
    Task(int x, int y, char op, func_t func) : _x(x), _y(y), _op(op), _callback(func)
    {}
    std::string operator()()
    {
        int result = _callback(_x, _y, _op);
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = %d", _x, _op, _y, result);
        return buffer;
    }
    std::string toTaskString()
    {
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "%d %c %d = ?", _x, _op, _y);
        return buffer;
    }

private:
    int _x;
    int _y;
    char _op;
    func_t _callback;
};

const std::string oper = "+-*/%";

int mymath(int x, int y, char op)
{
    int result = 0;
    switch (op)
    {
    case '+':
        result = x + y;
        break;
    case '-':
        result = x - y;
        break;
    case '*':
        result = x * y;
        break;
    case '/':
    {
        if (y == 0)
        {
            std::cerr << "div zero error!" << std::endl;
            result = -1;
        }
        else
            result = x / y;
    }
    break;
    case '%':
    {
        if (y == 0)
        {
            std::cerr << "mod zero error!" << std::endl;
            result = -1;
        }
        else
            result = x % y;
    }
    break;
    default:
        // do nothing
        break;
    }

    return result;
}

修改main函数

#include "BlockQueue.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <ctime>

void *consumer(void *_bq)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(_bq);
    while (true)
    {
        // 消费活动
        Task t;
        bq->pop(&t);
        std::string result = t();
        std::cout << "消费者,完成计算任务: " << result << std::endl;
        sleep(1);
    }
    return nullptr;
}

void *productor(void *_bq)
{
    BlockQueue<Task> *bq = static_cast<BlockQueue<Task> *>(_bq);
    while (true)
    {
        // 生产活动
        int x = rand() % 20 + 1;
        int y = rand() % 10;
        int operCode = rand() % oper.size();
        Task t(x, y, oper[operCode], mymath);
        bq->push(t);
        std::cout << "生产者,生产计算任务: " << t.toTaskString() << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    srand((unsigned long)time(nullptr) ^ 0x1122334);

    // 阻塞队列:共享资源
    BlockQueue<Task> *bq = new BlockQueue<Task>();

    // c:consumer p:productor
    pthread_t c, p;
    pthread_create(&c, nullptr, consumer, bq);
    pthread_create(&p, nullptr, productor, bq);

    // 线程等待
    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    return 0;
}

编译运行

--------------------- END ----------------------

「 作者 」 枫叶先生
「 更新 」 2023.6.1
「 声明 」 余之才疏学浅,故所撰文疏漏难免,
          或有谬误或不准确之处,敬请读者批评指正。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/598229.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

实用调试技巧与案例分析

目录 调试(Debug)&#xff1a; 调试的基本步骤&#xff1a; Debug和Release的介绍&#xff1a; 几个常用的快捷键&#xff1a; 案例一&#xff1a; 案例二&#xff1a; 如何写出好(易于调试)的代码&#xff1f; 案例一&#xff1a; 1.assert用法 2.const用法 案例二…

离散数学-数理逻辑

《离散数学》是计算机专业的一门十分重要的专业基础课。离散数学作为有力的数学工具对计算机的发展、计算机研究起着重大的作用。目前&#xff0c;计算机科学中普通采用离散数学中的一些基本概念、基本思想和基本方法。通过本课程的学习&#xff0c;掌握数理逻辑、集合论、代数…

6月1号软件资讯更新合集......

Chrome 114 正式发布&#xff0c;支持 CHIPS 自 Chrome 113 发布以来&#xff0c;已经过了四个星期&#xff0c;Google 近日也准时发布了 Chrome 114。Chrome 114 默认启用了 CHIPS&#xff0c;这是 Google 通过新的 cookie 属性来淘汰第三方 Cookie 的一部分&#xff1b;Chro…

利用Git及GitHub对项目进行版本控制

目录 一、在本地安装Git 二、利用Git将项目上传到Github上 三、用HTTPS获取GitHub上的项目 四、版本控制 一、在本地安装Git 1、Git安装链接&#xff1a;https://git-scm.com/downloads 2、下载安装包&#xff0c;双击exe文件进行安装&#xff1a; 3、接下来会弹出一系列…

Nginx服务基础、访问控制、虚拟主机

Nginx服务基础、访问控制、虚拟主机 一、Nginx介绍二、Linux系统Nginx安装1、官网下载Nginx压缩包2、编译安装Nginx1.配置环境2.安装依赖包3.创建运行用户、组4.编译安装5.检查、启动、重启、停止 nginx 服务6.添加Nginx系统服务 三、Nginx配置文件1、全局配置2、I/O 事件配置3…

水务漏损管理中存在的问题及解决方法

原文链接https://mp.weixin.qq.com/s?__bizMzg3NzkxNTI1MA&mid2247484559&idx1&snd1402e3f9fc75f7483a9dca3fc0174d4&chksmcf1af992f86d7084c48ce7e4072fd6be0555ec086c1065ef83398390c8bd19f2560daf594d7c&token955052059&langzh_CN#rd 在供水建设管…

Codeforces Round 875 div.2 problemB. Array merging题解

目录 一、题目 二、题目分析 三、 一、题目 传送门 B. Array merging time limit per test 1 second memory limit per test 256 megabytes input standard input output standard output You are given two arrays a and b both of length n. You will merge††…

Python获取各大企业招聘需求以及可视化分析展示

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 课程亮点 1、爬虫的基本流程 2、可视化分析展示 3、requests模块的使用 4、保存csv 开发环境: python 3.8 运行代码 pycharm 2022.3.2 辅助敲代码 专业版 模块使用: 内置模块: import pprint >>> 格式化输入…

Linux提权:定时任务 环境变量 配置不当 数据库

目录 环境变量配合SUID 实战测试 原理分析 实战中如何发现挖掘这类安全问题 定时任务打包配合SUID 原理分析 定时任务权限配置不当 数据库提权-梭哈的艺术 隧道出网提权 环境变量配合SUID 实战测试 这个提权方法比较鸡肋&#xff0c;因为它需要两个前提条件&#xf…

C语言:函数栈帧

寄存器&#xff1a; eax ebx ecx edx ebp esp 必须理解这两个寄存器&#xff08;寄存器是独立于内存的&#xff09; 这两个寄存器存放的是地址&#xff0c;用来维护函数栈帧&#xff08;正在调用哪个函数&#xff0c;两个寄存器就维护哪个函数的栈帧&#xff09; 每一个函数都…

C# NX二次开发:实现制图模块复制Sheet页,并且获取其中表格

今天要讲的是如何在NX中实现制图模块当前Sheet页的复制&#xff0c;并且获取Sheet页中的表格。首先简单介绍一下NX的制图模块是做什么的。 在NX中建模模块是用来绘制模型的&#xff0c;而想要将模型的相关尺寸投出来直观的看到&#xff0c;就要用到制图模块。 在制图模块中可…

leetcode--从二叉搜索树到更大和树(java)

从二叉搜索树到更大和树 leetcode -1038 题 从二叉搜索树到更大和树解题思路代码演示二叉树专题 leetcode -1038 题 从二叉搜索树到更大和树 原题链接&#xff1a; https://leetcode.cn/problems/binary-search-tree-to-greater-sum-tree/ 题目描述 给定一个二叉搜索树 root (B…

Chrome提示由贵单位管理该怎么取消?

如果你的 Chrome处于托管&#xff0c;你的管理员是可以设置或限制一些特定功能、可以安装一些应用、监视活动以及控制您的使用方式。 如何知道是否托管&#xff1a; 打开 Chrome 。在右上角&#xff0c;选择“更多”图标 。查看菜单底部。如果您看到“由贵单位管理”&#xff…

泡利矩阵(一)

〇、厄米矩阵 厄米矩阵&#xff08;Hermitian Matrix&#xff09;&#xff0c;也称为自共轭矩阵&#xff08;Self-adjoint Matrix&#xff09;&#xff0c;是线性代数中的一个重要概念。它是指一个复数域上的方阵&#xff0c;其转置矩阵与共轭矩阵相等。 具体来说&#xff0c…

15稳压二级管

目录 一、基本原理 二、I-V特性 三、工作原理 四、参数 1、Vz 2、Zzt和Zzk 3、IrVr 4、VfIf 5、Pd 五、应用 1、示例1 2、串联应用 3、钳位电路 六、动态电阻 一、基本原理 稳压二极管或“击穿二极管”(有时也称为齐纳二极管)基本上与标准PN结二极管相同&#xf…

Linux NGINX服务

NGINX与Apache对比 轻量级&#xff0c;Nginx比apache 占用更少的内存及资源&#xff1b;静态处理&#xff0c;Nginx 静态处理性能比 Apache 高 &#xff1b;Nginx可以实现无缓存的反向代理加速&#xff0c;提高网站运行速度&#xff1b;Nginx的性能和可伸缩性不依赖于硬件&…

multipass基础入门,搭建本地迷你云,一个比VMware轻量的虚拟机软件

介绍 multipass是一款轻量&#xff0c;且开源的虚拟机。 Multipass是一个灵活、强大的工具&#xff0c;可用于多种用途。在其最简单的形式下&#xff0c;它可以用来在任何主机上快速创建和销毁Ubuntu虚拟机&#xff08;实例&#xff09;。在更全面的情况下&#xff0c;Multip…

redhat9 shell脚本判断磁盘、判断web运行、curl测试web(及一些报错纠正)

1、判断当前磁盘剩余空间是否有20G&#xff0c;如果小于20G&#xff0c;则将报警邮件发送给管理员&#xff0c;每天检查一次磁盘剩余空间。 2、判断web服务是否运行&#xff08;1、查看进程的方式判断该程序是否运行 2、通过查看端口的方式判断该程序是否运行&#xff09;&…

关于这款开源的ES的ORM框架-Easy-Es适合初学者入手不?

前言 最近笔者为了捡回以前自学的ES知识&#xff0c;准备重新对ES的一些基础使用做个大致学习总结。然后在摸鱼逛开源社区时无意中发现了一款不错的ElasticSearch插件-Easy-ES&#xff0c;可称之为“ES界的MyBatis-Plus”。联想到之前每次用RestHighLevelClient写一些DSL操作时…

微信小程序 nodejs+vue+python家校通家校联系作业系统

家本系统有家长&#xff0c;教师&#xff0c;管理员三个角色&#xff0c;家长可以注册登陆小系统&#xff0c;查看公告&#xff0c;查看教师布置的作业&#xff0c;上传孩子的作业&#xff0c;查看学生成绩&#xff0c;成绩统计&#xff0c;家长在线发贴交流&#xff0c;在线留…