【Linux多线程】基于生产消费模型写的一个实例(附源代码+讲解)

news2024/11/23 8:04:30

00

生产消费模型

  • 生产消费模型
    • 为何要使用生产者消费者模型
    • 生产者消费者模型优点
  • 基于BlockingQueue的生产者消费者模型
    • BlockQueue.cc
      • 代码
      • 解释
    • BlockQueue.hpp
      • 代码
      • 解释
    • Makefile
      • 代码
      • 解释
    • Task.hpp
      • 代码
      • 解释

生产消费模型

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

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

生产者消费者模型优点

  • 解耦
  • 支持并发
    这里的支持并发并不是说在临界区(一般情况下都不是在临界区发生的并发),而是 生产前 和消费后两个状态下的并发 举例子:
    生产前的准备 和消费后的处理 这两个状态就是并发的!!!
  • 支持忙闲不均
    00

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

BlockQueue.cc

代码

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

const std::string ops = "+-*/%";
//生产者
void *productor(void *args)
{
    BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        // 1. 制作任务 --- 要不要花时间?? -- 网络,磁盘,用户
        int one = rand() % 50;
        int two = rand() % 20;
        char op = ops[rand() % ops.size()];
        Task t(one, two, op);
        // 2. 生产任务
        bqp->push(t);
        cout << "producter[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 生产了一个任务: " << one << op << two << "=?" << endl;
        sleep(1);
    }
}


//消费者
void *consumer(void *args)
{
  BlockQueue<Task> *bqp = static_cast<BlockQueue<Task> *>(args);
    while (true)
    {
        Task t = bqp->pop(); // 消费任务
        int result = t();    //处理任务 --- 任务也是要花时间的!
        int one, two;
        char op;
        t.get(&one, &two, &op);
        cout << "consumer[" << pthread_self() << "] " << (unsigned long)time(nullptr) << " 消费了一个任务: " << one << op << two << "=" << result << endl;
    }
}


int main()
{
     srand((unsigned long)time(nullptr) ^ getpid());
    // 定义一个阻塞队列
    // 创建两个线程,productor, consumer
    // productor -----  consumer
    // BlockQueue<int> bq;
    // bq.push(10);
    // int a = bq.pop();
    // cout << a << endl;
    // 既然可以使用int类型的数据,我们也可以使用自己封装的类型,包括任务
    // BlockQueue<int> bq;
    BlockQueue<Task> bq;

    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;
}

解释

  • 代码是一个简单的多线程程序,涉及生产者-消费者模型。
  1. 首先,在代码中定义了一个BlockQueue类,它是一个串行,用于在生产者和消费者之间提交任务。
  2. 接下来,定义了一个Task类,用于表示任务。任务对象包含两个整数和一个字符,用于表示要执行的操作。
  3. 在main函数中,首先使用srand函数初始化随机数种子,以确保随机数的生成。
  4. 然后创建了一个BlockQueue对象bq,作为生产者和消费者之间的共享队列。
  5. 通过调用pthread_create函数创建两个线程c和p,分别用于消费者和生产者。同时,将共享队列bq传递给这两个线程,以便它们可以访问和操作队列中的任务。
  6. 在消费者线程函数consumer中,通过调用bqp->pop()方法从队列中取出一个任务t,然后执行该任务,计算结果输出并。消费者线程将一直循环执行这个过程。
  7. 在生产者线程函数中productor,首先生成随机的两个整数和一个操作符,然后创建一个任务对象t表示这个任务。接着,将该任务对象t推入共享队列bq中,表示生产了一个任务。生产者线程将一直循环执行这个过程。
  8. 最后,主线程使用pthread_join函数等待两个线程c并p结束,然后退出程序。
  • 总体来说,大概代码展示了多线程编程中的生产者-消费者模型,其中一个线程生产任务并推入共享队列,另一个线程从队列中取出任务并执行。通过使用互斥锁和条件变量,保证了线程之间的同步与交互,使得生产者和消费者可以安全地协作。

BlockQueue.hpp

代码

#pragma once
#include <iostream>
#include <queue>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>

using namespace std;
const uint32_t gDefaulcap=5;//创建的线程数   用uint32_t 保证非负
template <class T>
class BlockQueue
{
    public:
    BlockQueue(uint32_t cap=gDefaulcap):cap_(cap) //构造初始化
    {
         pthread_mutex_init(&mutex_, nullptr);
        pthread_cond_init(&conCond_, nullptr);
        pthread_cond_init(&proCond_, nullptr);
    }

    ~BlockQueue()
    {
        pthread_mutex_destroy(&mutex_);
        pthread_cond_destroy(&conCond_);
        pthread_cond_destroy(&proCond_);
    }
    //生产者
    void push(const T &in)
    {
        // 加锁
        // 判断->是否适合生产->bq是否为满->程序员视角的条件->1. 满(不生产) 2. 不满(生产)
        // if(满) 不生产,休眠
        // else if(不满) 生产,唤醒消费者
        // 解锁

        lockQueue();
        while (isFull()) // ifFull就是我们在临界区中设定的条件
        {
            // before: 当我等待的时候,会自动释放mutex_
            proBlockWait(); //阻塞等待,等待被唤醒。 被唤醒 != 条件被满足(概率虽然很小),被唤醒 && 条件被满足
            // after: 当我醒来的时候,我是在临界区里醒来的!!
        }
        // 条件满足,可以生产
        pushCore(in); //生产完成
        // wakeupCon(); // 唤醒消费者
        unlockQueue();
        wakeupCon(); // 唤醒消费者
    }
    //消费者
T pop()
    {
        // 加锁
        // 判断->是否适合消费->bq是否为空->程序员视角的条件->1. 空(不消费) 2. 有(消费)
        // if(空) 不消费,休眠
        // else if(有) 消费,唤醒生产者
        // 解锁
        lockQueue();
        while (isEmpty())
        {
            conBlockwait(); //阻塞等待,等待被唤醒,?
        }
        // 条件满足,可以消费
        T tmp = popCore();
        unlockQueue();
        wakeupPro(); // 唤醒生产者

        return tmp;
    }

    private:
    void lockQueue()
    {
        pthread_mutex_lock(&mutex_);
    }
    void unlockQueue()
    {
        pthread_mutex_unlock(&mutex_);
    }
    bool isEmpty()
    {
        return bq_.empty();
    }
    bool isFull()
    {
        return bq_.size() == cap_;
    }
    void proBlockWait() // 生产者一定是在临界区中的!
    {
        // 1. 在阻塞线程的时候,会自动释放mutex_锁
        pthread_cond_wait(&proCond_, &mutex_);
    }

    void conBlockwait() //阻塞等待,等待被唤醒
    {
        // 1. 在阻塞线程的时候,会自动释放mutex_锁
        pthread_cond_wait(&conCond_, &mutex_);
        // 2. 当阻塞结束,返回的时候,pthread_cond_wait,会自动帮你重新获得mutex_,然后才返回
        // 为什么我们上节课,写的代码,批量退出线程的时候,发现无法退出?
    }

    void wakeupPro() // 唤醒生产者
    {
        pthread_cond_signal(&proCond_);
    }
    void wakeupCon() // 唤醒消费者
    {
        pthread_cond_signal(&conCond_);
    }
    void pushCore(const T &in)
    {
        bq_.push(in); //生产完成
    }
    T popCore()
    {
        T tmp = bq_.front();
        bq_.pop();
        return tmp;
    }

    private:
    uint32_t cap_;           //容量
    queue<T> bq_;            // blockqueue
    pthread_mutex_t mutex_;  //保护阻塞队列的互斥锁
    pthread_cond_t conCond_; // 让消费者等待的条件变量
    pthread_cond_t proCond_; // 让生产者等待的条件变量
};

解释

这是一个模板类,实现了一个线程安全的阻塞队列(BlockQueue)。阻塞队列是一个在并发编程中常见的数据结构,它可以在多个线程之间安全地传递数据。在这个类中,提供了两个主要的操作,push()和pop(),分别表示向队列中添加数据和从队列中获取数据。当队列满时,push()操作将会阻塞,直到有空间可以添加数据。同理,当队列为空时,pop()操作将会阻塞,直到有数据可以获取。

接下来,我们逐个解释一下代码中的各个部分:

  • 成员变量:

cap_:队列的容量。
bq_:队列本身,用于存储数据。
mutex_:用于保护阻塞队列的互斥锁,防止多个线程同时操作队列。
conCond_和proCond_:分别表示消费者和生产者的条件变量。它们用于在特定条件下让线程等待,比如队列为空或队列已满。
成员函数:

BlockQueue()和~BlockQueue():构造函数和析构函数,用于初始化和销毁互斥锁以及条件变量。
push():向队列中添加数据。如果队列已满,该函数将阻塞,直到队列中有可用空间。
pop():从队列中获取数据。如果队列为空,该函数将阻塞,直到队列中有可用数据。
lockQueue()和unlockQueue():用于加锁和解锁队列。
isEmpty()和isFull():用于检查队列是否为空或已满。
proBlockWait()和conBlockwait():用于在特定条件下让生产者和消费者线程等待。
wakeupPro()和wakeupCon():用于唤醒等待的生产者和消费者线程。
pushCore()和popCore():分别表示实际的添加数据和获取数据的操作。

这个阻塞队列可以用于生产者-消费者模型,在多线程编程中,生产者将数据放入队列,消费者从队列中获取数据,由于队列的线程安全性,可以保证数据的一致性。同时,通过条件变量的使用,实现了当队列满或空时,相应的生产者线程或消费者线程会阻塞等待,直到条件满足(即有空间放入数据或有数据可取)时再继续执行

Makefile

代码

CC=g++
FLAGS=-std=c++11
LD=-lpthread
bin=BlockQueue
src=BlockQueueTest.cc

$(bin):$(src)
	$(CC) -o $@ $^ $(LD) $(FLAGS)
.PHONY:clean
	rm -f $(bin)

解释

CC=g++:定义编译器为g++。
FLAGS=-std=c++11:定义编译标志,这里是指定使用C++11标准。
LD=-lpthread:定义链接标志,这里是链接pthread库。
bin=BlockQueue:定义目标可执行文件的名字为BlockQueue。
src=BlockQueueTest.cc:定义源文件名为BlockQueueTest.cc。
接下来的部分定义了如何生成目标文件:

( b i n ) : (bin): (bin):(src):表示目标文件依赖于源文件,当源文件有任何改动时,目标文件都需要重新生成。
$(CC) -o $@ $^ $(LD) ( F L A G S ) :定义了生成目标文件的命令。 (FLAGS):定义了生成目标文件的命令。 (FLAGS):定义了生成目标文件的命令。(CC)是编译器,-o @ 表示生成的目标文件名, @表示生成的目标文件名, @表示生成的目标文件名,^表示所有的依赖文件, ( L D ) 表示链接标志, (LD)表示链接标志, (LD)表示链接标志,(FLAGS)表示编译标志。总的来说,这条命令的意思是使用g++编译器编译源文件并链接pthread库,生成的目标文件名为BlockQueue。
最后,定义了一个伪目标clean,它不代表任何文件,主要用于清理项目:

.PHONY:clean:声明clean为伪目标。这样,make就不会去检查是否存在一个叫做clean的文件,而是每次都执行clean规则。
rm -f $(bin):删除生成的目标文件。rm是删除文件的命令,-f参数表示强制删除。

总的来说,这个Makefile文件定义了如何编译项目和清理项目,让编译和链接过程更加自动化

Task.hpp

代码

#pragma once

#include <iostream>
#include <string>

class Task
{
public:
    Task() : elemOne_(0), elemTwo_(0), operator_('0')
    {
    }
    Task(int one, int two, char op) : elemOne_(one), elemTwo_(two), operator_(op)
    {
    }
    int operator() ()
    {
        return run();
    }
    int run()
    {
        int result = 0;
        switch (operator_)
        {
        case '+':
            result = elemOne_ + elemTwo_;
            break;
        case '-':
            result = elemOne_ - elemTwo_;
            break;
        case '*':
            result = elemOne_ * elemTwo_;
            break;
        case '/':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "div zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ / elemTwo_;
            }
        }

        break;
        case '%':
        {
            if (elemTwo_ == 0)
            {
                std::cout << "mod zero, abort" << std::endl;
                result = -1;
            }
            else
            {
                result = elemOne_ % elemTwo_;
            }
        }
        break;
        default:
            std::cout << "非法操作: " << operator_ << std::endl;
            break;
        }
        return result;
    }
    int get(int *e1, int *e2, char *op)
    {
        *e1 = elemOne_;
        *e2 = elemTwo_;
        *op = operator_;
    }
private:
    int elemOne_;
    int elemTwo_;
    char operator_;
};

解释

这是一个名为Task的C++类。这个类封装了一种任务,具体来说,就是一个包含两个整数和一个运算符的简单数学运算。类的主要功能由run()函数实现,它根据operator_的值来决定应执行哪种运算,并返回结果。以下是各个部分的详细解释:

成员变量:

elemOne_ 和 elemTwo_:这是两个需要进行运算的整数。
operator_:这是一个字符,表示应执行的运算类型。
构造函数:

Task():默认构造函数,将两个整数初始化为0,运算符初始化为字符’0’。
Task(int one, int two, char op):参数化构造函数,允许在创建对象时指定两个整数和运算符的初始值。
成员函数:

operator()():函数调用运算符重载,使得该类的对象可以像函数一样被调用,实际上调用了run()函数。
run():执行运算并返回结果。如果operator_是’+‘、’-‘、’*‘、’/‘或’%‘,则执行相应的运算。如果operator_是’/‘或’%',并且elemTwo_是0,则打印一条错误信息并返回-1。如果operator_是其他值,则打印一条错误信息并继续运行。
get(int *e1, int *e2, char *op):获取elemOne_、elemTwo_和operator_的值,并将它们分别存储在e1、e2和op指向的内存中。
这个类可以用于表示和执行简单的二元运算任务,例如:

Task task(10, 2, '/');
int result = task();  // result is 5

这将创建一个任务,它将10除以2,并返回结果。

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

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

相关文章

tdengine入门详解

TDengine是什么&#xff1f; TDengine 是一款开源、高性能、云原生的时序数据库&#xff08;Time Series Database, TSDB&#xff09;, 它专为物联网、车联网、工业互联网、金融、IT 运维等场景优化设计&#xff0c;基于C语言开发。 什么是时序数据库&#xff1f;时序数据产生…

三元运算符引发的自动拆装箱问题

文章目录 问题背景问题排查排查过程问题扩展总结 问题背景 生产环境上出现空指针异常&#xff0c;追踪报错位置得知以下代码报错 if (isNull(aiGroup)) {return null;}aiGroup.setNum(isNull(param.getNum()) ? aiGroup.getNum() : param.getNum().doubleValue());问题排查 …

1500-2000元预算性价比吉他推荐,雅马哈FG800和VEAZEN费森VZ90怎么选?评测对比哪一款更适合初学者入门选购!

在2000元价位入门进阶吉他圈里&#xff0c;可谓是群雄角逐&#xff0c;Yamaha 雅马哈入门级FG800系列和VEAZEN 费森VZ90系列是一直都很热销的面单吉他型号&#xff0c;初学者想要在其中挑选出一把合适自己的吉他还是有点难度的。 那么&#xff0c;今天就以它们为本期的评测主角…

企业级高负载web服务器-Tomcat小项目

目录 web静态动态页面区别安装java环境安装Tomcat安装Tomcat包到目录查看Tomcat主目录结构查看Tomcat配置目录结构Tomcat管理Tomcat web管理功能 部署jpress应用 web静态动态页面区别 静态页面&#xff1a; 在网站设计中&#xff0c;纯粹HTML格式的网页&#xff08;可以包含图…

后端整理(JVM、Redis、反射)

1. JVM 文章仅为自身笔记 详情查看一篇文章掌握整个JVM&#xff0c;JVM超详细解析&#xff01;&#xff01;&#xff01; 1.1 什么是JVM jvm是Java虚拟机 1.2 Java文件的编译过程 程序员编写代码形成.java文件经过javac编译成.class文件再通过JVM的类加载器进入运行时数据…

MFC、Qt、WPF?该用哪个?

MFC、Qt和WPF都是流行的框架和工具&#xff0c;用于开发图形用户界面&#xff08;GUI&#xff09;应用程序。选择哪个框架取决于你的具体需求和偏好。MFC&#xff08;Microsoft Foundation Class&#xff09;是微软提供的框架&#xff0c;使用C编写&#xff0c;主要用于Windows…

蔚小理新势力互联网造车在CAN FD硬件主框架及后装控制方案开发

在国内&#xff0c;新势力造车影响已经非常之大&#xff0c;整个造车大潮中&#xff0c;新整车企业蔚来汽车、小鹏汽车、理想汽车无一例外选择了CAN FD作为主要的车载通信总线&#xff0c;特斯拉推出了引领汽车EE架构集中化的趋势&#xff0c;即使在车载以太网EE架构快速发展的…

BES 平台 SDK之充电盒与耳塞串口单工通信

本文章是基于BES2700 芯片&#xff0c;其他BESxxx 芯片可做参考&#xff0c;如有不当之处&#xff0c;欢迎评论区留言指出。仅供参考学习用&#xff01; BES 平台 SDK之主从耳组队_谢文浩的博客-CSDN博客 关于系统主从耳组队流程可参考上一篇文章。链接如上所示&#xff01; …

初识集合和背后的数据结构

目录 集合 Java集合框架 数据结构 算法 集合 集合&#xff0c;是用来存放数据的容器。其主要表现为将多个元素置于一个单元中&#xff0c;用于对这些元素进行增删查改。例如&#xff0c;一副扑克牌(一组牌的集合)、一个邮箱(一组邮件的集合&#xff09;。 Java中有很多种集…

捷码低代码|Modal模态框组件详解

知识补充&#xff1a; 模态组件是一种在用户界面中显示的特殊类型的组件。它们被设计为在应用程序的其他部分被屏蔽或暂停的情况下引导用户完成一个特定的任务或交互。 常见的模态组件包括&#xff1a; 1、弹出窗口&#xff08;Popup&#xff09;&#xff1a;弹出窗口是一种常见…

Unity中UGUI的 OnPopulateMesh函数与VertexHelper类

Graphics类 当一个UGUI的UI元素生成顶点数据时会调用Graphics类中的 OnPopulateMesh(VertexHelper vh) 函数&#xff0c;我们可以在这个函数中修改顶点的数据或者获取顶点的数据。 UGUI中与显示相关的控件&#xff0c;例如Image、Text、RawImage等都继承自MaskableGraphic类&a…

正则替换windows文件名禁用的特殊字符

背景&#xff1a; windows文件名中不能出现以下提示的特殊字符&#xff0c;因此需要提前替换处理。 解决&#xff1a; // 替换\/:*?"<>|为空 fileName.replaceAll("[\\\\/:*?\"<>|]", "");

Boost开发指南-3.9object_pool

object_pool object_pool是用于类实例&#xff08;对象&#xff09;的内存池&#xff0c;它的功能与pool类似&#xff0c;但会在析构时对所有已经分配的内存块调用析构函数&#xff0c;从而正确地释放资源。 object_pool位于名字空间boost&#xff0c;为了使用object_pool组件…

千元级入门单板吉他推荐,SAGA萨伽SF700、VEAZEN费森VZ200、布鲁克V12、恩雅X1PRO全方面评测对比,哪一款更值得购买!

很多吉他初学者的预算不多&#xff0c;就想要选购平价又好用的吉他&#xff0c;这个想法是很正确的。初学者要注意的是这种平价且高性价比的吉他需要仔细挑选&#xff0c;太便宜的合板吉他保证不了原材料的品质和制作工艺要求&#xff0c;音准手感都无法保证&#xff0c;那么这…

云主机秘钥泄露及利用

前言&#xff1a; 云平台作为降低企业资源成本的工具&#xff0c;在当今各大公司系统部署场景内已经成为不可或缺的重要组成部分&#xff0c;并且由于各类应用程序需要与其他内外部服务或程序进行通讯而大量使用凭证或密钥&#xff0c;因此在漏洞挖掘过程中经常会遇到一类漏洞&…

时间复杂度、空间复杂度实践练习(力扣OJ)

目录 文章目录 前言 题目一&#xff1a;轮转数组 思路一&#xff1a; 思路二&#xff1a; 思路三&#xff1a; 题目二&#xff1a;消失的数字 思路一&#xff1a; 思路二&#xff1a; 思路三&#xff1a; 题目三&#xff1a;移除元素 思路&#xff1a; 总结 前言 想要编写高效的…

2023年第四届“华数杯”数学建模思路 - 案例:随机森林

## 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 什么是随机森林&#xff1f; 随机森林属于 集成学习 中的 Bagging&#xff08;Bootstrap AGgregation 的简称&#xff09; 方法。如果用图来表示他们之…

【技术分享】SSD20X USB摄像头使用

本文主要介绍基于Purple Pi R1演示如何配置USB摄像头&#xff0c;此方法适用于SSD201/202全系列产品。 Purple Pi R1主板&#xff0c;是基于 SigmaStar SSD201 SoC&#xff08;ARM Cortex A7 内核&#xff09;兼容树莓派的开发板&#xff0c;主频高达1.2GHz&#xff0c;256KB …

HR如何提高自己的薪资?或许是一个好选择!

从助理到总监&#xff0c;随着级别的提升&#xff0c;薪资也水涨船高&#xff0c;从4K涨到了24K。值得注意的是&#xff0c;从助理到主管&#xff0c;薪资涨幅较小&#xff0c;而从主管到总监&#xff0c;尤其是经理到总监&#xff0c;薪资有很大的突破。 各行业HR人员薪资水平…

Hutool BeanUtils.copyProperties的四种用法 空不拷贝/忽略拷贝/空不和忽略拷贝/全拷贝

关注公众号&#xff1a;”奇叔码技术“ 回复&#xff1a;“java面试题大全”或者“java面试题” 即可领取资料 一、Hutool BeanUtils.copyProperties的四种用法 空不拷贝/忽略拷贝/空不和忽略拷贝/全拷贝 1、第一种用法&#xff1a; BeanUtils.copyProperties(三个参数) 不为空…