【Linux | C++ 】生产者消费者模型(Linux系统下C++ 代码模拟实现)

在这里插入图片描述

阅读导航

  • 引言
  • 一、生产者消费者问题
    • 🍁将生产者消费者模型比喻为超市的顾客和供货商
  • 二、C++ queue模拟阻塞队列的生产消费模型(伪代码)
  • 三、RAII风格的加锁方式
    • 1. 简介
    • 2. 示例
  • 四、基于Linux操作系统使用C++代码,采用RAII风格的加锁方式模拟“生产者消费者模型”
    • ⭕Makefile文件
    • ⭕ . h 头文件
      • ✅lockGuard.h
      • ✅BlockQueue.h
      • ✅Task.h
    • ⭕ . cpp 文件
      • ✅ConProd.cpp
  • 温馨提示

引言

多线程编程中的同步问题是一个普遍存在的难点,为了解决这些问题,开发者们设计出了各种同步机制,如条件变量、信号量、互斥锁等。生产者消费者模型是一个经典案例,它涉及到两类线程:生产者和消费者。本文将介绍如何使用条件变量来实现生产者消费者模型,帮助读者更好地理解多线程编程中的同步机制和技术。

一、生产者消费者问题

生产者线程负责生产数据或物品,并将它们放入一个共享缓冲区中。而消费者线程负责从缓冲区中获取这些数据或物品,并进行相应的处理。在这个过程中,需要保证生产者和消费者之间的正确协作和数据安全,以避免数据竞争和不可预测的结果。

为了解决这个问题,我们需要使用同步机制来协调两种类型的线程之间的操作。最常见的同步机制包括条件变量、信号量、互斥锁等。这些机制可以保证线程之间的正确协作和数据安全,避免数据竞争和死锁等问题的发生。
在这里插入图片描述

在生产者消费者问题中,同步机制的主要作用是保证缓冲区的数据安全和正确性。当缓冲区已满时,生产者线程需要等待一段时间,直到缓冲区有足够的空间可以放置新数据;而当缓冲区为空时,消费者线程需要等待一段时间,直到缓冲区有新数据可以获取。这种等待和通知的机制可以使用条件变量来实现。

🍁将生产者消费者模型比喻为超市的顾客和供货商

当我们将生产者消费者模型比喻为超市的顾客和供货商时,可以清晰地理解这一概念。假设超市是一个缓冲区,顾客是消费者,供货商是生产者。供货商不断地向超市提供新货物(产品),而顾客则从超市购买这些货物。在这个过程中,超市需要保证货物的充足和有序销售,而且顾客和供货商之间的操作需要协调。
在这个例子中,生产者不断地往超市里补充货物,当超市库存已满时,供货商需要等待一段时间,直到有空间放入新货物。而消费者则不断地从超市购买货物,当超市库存为空时,顾客需要等待新货物的到来。

⭕通过这个例子,我们可以清晰地看到生产者消费者模型中的关键概念:生产者负责生产物品并放入缓冲区,消费者负责从缓冲区获取物品并进行消费,而缓冲区则需要合理地协调生产者和消费者之间的操作,以避免过度生产或过度消费的情况发生。这种协调工作正是多线程编程中同步机制的核心应用之一

在这里插入图片描述

🚨注意在使用条件变量等同步机制时,需要保证线程之间的正确协作,避免死锁和饥饿等问题的发生。同时,还需要考虑性能优化等问题,以提高程序的效率和响应速度

二、C++ queue模拟阻塞队列的生产消费模型(伪代码)

以下是使用C++实现基于std::queuestd::mutex的生产者消费者模型的示例代码:

#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

std::queue<int> dataQueue;
std::mutex mtx;
std::condition_variable cv;

void producer()
{
    for (int i = 1; i <= 10; ++i) {
        std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟生产数据的耗时操作

        {
            std::lock_guard<std::mutex> lock(mtx);
            dataQueue.push(i);
            std::cout << "Produced: " << i << std::endl;
        }

        cv.notify_one(); // 通知消费者线程有新数据可用
    }
}

void consumer()
{
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);

        // 使用条件变量等待,直到有新数据可用
        cv.wait(lock, [] { return !dataQueue.empty(); });

        int num = dataQueue.front();
        dataQueue.pop();
        std::cout << "Consumed: " << num << std::endl;

        lock.unlock();

        if (num == 10) {
            break; // 结束消费者线程,当消费到数字10时退出
        }
    }
}

int main()
{
    std::thread producerThread(producer);
    std::thread consumerThread(consumer);

    producerThread.join();
    consumerThread.join();

    return 0;
}

在这个示例中,生产者线程将数字从1到10放入std::queue中,而消费者线程从std::queue中取出这些数字进行消费。通过使用std::mutexstd::condition_variable,我们实现了线程之间的同步和通信。

生产者线程使用std::lock_guard<std::mutex>锁住互斥量,并将数据放入队列后通知消费者线程。消费者线程在等待条件变量时会解锁互斥量,以允许其他线程访问数据队列。当有新数据可用时,消费者线程被唤醒,并继续处理数据。

三、RAII风格的加锁方式

1. 简介

RAII(Resource Acquisition Is Initialization)是一种C++编程风格,通过在对象的构造函数中获取资源,在析构函数中释放资源,从而实现资源的自动管理。在多线程编程中,RAII可以用于实现加锁和解锁的自动管理,确保锁的正确释放,避免忘记手动解锁而导致的死锁或资源泄漏。

2. 示例

#include <iostream>
#include <thread>
#include <mutex>

class LockGuard {
public:
    explicit LockGuard(std::mutex& mtx) : mutex(mtx) {
        mutex.lock();
    }

    ~LockGuard() {
        mutex.unlock();
    }

private:
    std::mutex& mutex;
};

std::mutex mtx;

void someFunction() {
    LockGuard lock(mtx); // 在作用域中创建LockGuard对象,自动加锁

    // 执行需要加锁保护的操作
    std::cout << "Critical section" << std::endl;

    // 当LockGuard对象离开作用域时,会自动调用析构函数解锁
}

int main() {
    std::thread thread1(someFunction);
    std::thread thread2(someFunction);

    thread1.join();
    thread2.join();

    return 0;
}

在这个示例中,我们定义了一个名为LockGuard的RAII类,它在构造函数中获取一个std::mutex的引用,并在析构函数中调用unlock()来解锁互斥量。在someFunction()中,我们通过创建LockGuard对象来实现加锁和解锁操作。当LockGuard对象离开作用域时,其析构函数会自动被调用,从而释放互斥量。

通过使用RAII风格的加锁方式,我们可以确保在进入临界区之前加锁,在离开临界区之后自动解锁,避免了手动控制加锁和解锁操作可能带来的错误。同时,由于RAII对象的生命周期与作用域相对应,因此可以确保在任何情况下都会正确释放资源,即使在函数发生异常或提前返回时也不例外。这种方式简化了代码,提高了程序的可靠性和可读性。

四、基于Linux操作系统使用C++代码,采用RAII风格的加锁方式模拟“生产者消费者模型”

⭕Makefile文件

cp:ConProd.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f cp

⭕ . h 头文件

✅lockGuard.h

#pragma once

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

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx):pmtx_(mtx)
    {}

    // 加锁操作
    void lock() 
    {
        std::cout << "要进行加锁" << std::endl;
        pthread_mutex_lock(pmtx_);
    }

    // 解锁操作
    void unlock()
    {
        std::cout << "要进行解锁" << std::endl;
        pthread_mutex_unlock(pmtx_);
    }

    ~Mutex()
    {}
private:
    pthread_mutex_t *pmtx_; // 互斥锁指针
};

// RAII风格的加锁方式
class lockGuard
{
public:
    lockGuard(pthread_mutex_t *mtx):mtx_(mtx)
    {
        mtx_.lock(); // 构造时进行加锁操作
    }

    ~lockGuard()
    {
        mtx_.unlock(); // 析构时进行解锁操作
    }

private:
    Mutex mtx_; // 互斥锁对象
};

✅BlockQueue.h

#pragma once

#include <iostream>
#include <queue>
#include <mutex>
#include <pthread.h>
#include "lockGuard.h"
const int gDefaultCap = 5; // 队列默认容量

template <class T>
class BlockQueue
{
private:
    bool isQueueEmpty() // 判断队列是否为空
    {
        return bq_.size() == 0;
    }
    bool isQueueFull() // 判断队列是否已满
    {
        return bq_.size() == capacity_;
    }

public:
    BlockQueue(int capacity = gDefaultCap) : capacity_(capacity)
    {
        // 初始化互斥锁和条件变量
        pthread_mutex_init(&mtx_, nullptr);
        pthread_cond_init(&Empty_, nullptr);
        pthread_cond_init(&Full_, nullptr);
    }

    void push(const T &in) // 生产者线程调用此函数向队列中添加元素
    {
        lockGuard lockgrard(&mtx_); // 自动调用构造函数,对互斥锁进行加锁

        while (isQueueFull()) // 如果队列已满,则阻塞当前线程,等待队列有空闲位置
            pthread_cond_wait(&Full_, &mtx_);

        bq_.push(in); // 将元素添加到队列尾部
        pthread_cond_signal(&Empty_); // 对等待在 Empty_ 上的线程发送信号,表示队列非空
    }

    void pop(T *out) // 消费者线程调用此函数从队列中取出元素
    {
        lockGuard lockguard(&mtx_); // 自动调用构造函数,对互斥锁进行加锁

        while (isQueueEmpty()) // 如果队列为空,则阻塞当前线程,等待队列有元素
            pthread_cond_wait(&Empty_, &mtx_);

        *out = bq_.front(); // 取出队头元素
        bq_.pop(); // 将元素从队列中删除
        pthread_cond_signal(&Full_); // 对等待在 Full_ 上的线程发送信号,表示队列未满
    }

    ~BlockQueue()
    {
        // 销毁互斥锁和条件变量
        pthread_mutex_destroy(&mtx_);
        pthread_cond_destroy(&Empty_);
        pthread_cond_destroy(&Full_);
    }

private:
    std::queue<T> bq_;     // 阻塞队列
    int capacity_;         // 容量上限
    pthread_mutex_t mtx_;  // 通过互斥锁保证队列安全
    pthread_cond_t Empty_; // 用它来表示队列是否空的条件
    pthread_cond_t Full_;  // 用它来表示队列是否满的条件
};

✅Task.h

#pragma once

#include <iostream>
#include <functional>

typedef std::function<int(int, int)> func_t;

class Task
{
public:
    // 默认构造函数
    Task() {}

    // 构造函数,初始化任务的参数和可调用对象
    Task(int x, int y, func_t func) : x_(x), y_(y), func_(func) {}

    // 重载函数调用运算符,用于执行任务
    int operator()()
    {
        return func_(x_, y_);
    }

public:
    int x_;         // 任务的参数 x
    int y_;         // 任务的参数 y
    func_t func_;   // 可调用对象,接受两个整数并返回一个整数
};

⭕ . cpp 文件

✅ConProd.cpp

#include "BlockQueue.h"
#include "Task.h"

#include <pthread.h>
#include <unistd.h>
#include <ctime>

// 定义一个加法函数,用于任务的处理
int myAdd(int x, int y)
{
    return x + y;
}

// 消费者线程函数,从阻塞队列中获取任务并完成任务
void* consumer(void *args)
{
    // 将参数转化为阻塞队列的指针
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;

    while(true)
    {
        // 获取任务
        Task t;
        bqueue->pop(&t);

        // 完成任务,并输出结果
        std::cout << pthread_self() <<" consumer: "<< t.x_ << "+" << t.y_ << "=" << t() << std::endl;
    }

    return nullptr;
}

// 生产者线程函数,制作任务并将任务加入阻塞队列
void* productor(void *args)
{
    // 将参数转化为阻塞队列的指针
    BlockQueue<Task> *bqueue = (BlockQueue<Task> *)args;

    while(true)
    {
        // 制作任务
        int x = rand()%10 + 1;
        usleep(rand()%1000);
        int y = rand()%5 + 1;
        Task t(x, y, myAdd);

        // 生产任务,并输出提示信息
        bqueue->push(t);
        std::cout <<pthread_self() <<" productor: "<< t.x_ << "+" << t.y_ << "=?" << std::endl;

        // 限制生产者的速度,以便观察阻塞队列的功能
        sleep(1);
    }

    return nullptr;
}

int main()
{
    // 随机数种子初始化
    srand((uint64_t)time(nullptr) ^ getpid() ^ 0x32457);

    // 创建一个阻塞队列
    BlockQueue<Task> *bqueue = new BlockQueue<Task>();

    // 创建两个消费者线程和两个生产者线程
    pthread_t c[2],p[2];
    pthread_create(c, nullptr, consumer, bqueue);
    pthread_create(c + 1, nullptr, consumer, bqueue);
    pthread_create(p, nullptr, productor, bqueue);
    pthread_create(p + 1, nullptr, productor, bqueue);

    // 等待所有线程结束
    pthread_join(c[0], nullptr);
    pthread_join(c[1], nullptr);
    pthread_join(p[0], nullptr);
    pthread_join(p[1], nullptr);

    // 释放阻塞队列内存
    delete bqueue;

    return 0;
}

温馨提示

感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!

再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

flutter的ListView和SingleChildScrollView有什么区别?他们的使用场景有什么不一样?

文章目录 简介ListViewSingleChildScrollView使用场景的不同 简介 ListView和SingleChildScrollView都是在Flutter中用于处理滚动内容的组件&#xff0c;但它们有一些关键的区别。 ListView 多个子元素&#xff1a; ListView是一个滚动的可滚动组件&#xff0c;通常用于包含多…

写字基本功 - 正确握笔姿势

写字基本功 - 正确握笔姿势 1. 写字基本功 郑文彬 (布衣) 先生 2. 正确握笔步骤 握笔姿势教学 http://www.bebosspen.com/index.php/correct 3. 正确握笔姿势 - 重点解说图 握笔姿势教学 http://www.bebosspen.com/index.php/correct 3.1. 食指 食指低于拇指 两段弯曲勿三…

【价值几十万的仿抖音直播电商系统源码共享】

当下&#xff0c;传统的图文电商模式已经走向没落&#xff0c;以抖音为首的直播电商模式备受用户追捧&#xff0c;它具有实时直播和强互动的特点&#xff0c;是传统电商所不具备的优势。而且&#xff0c;当前正是直播电商的红利期&#xff0c;很多主播和品牌商都通过直播电商业…

[Halcon模块] Halcon13.0查询算子模块归属

&#x1f4e2;博客主页&#xff1a;https://loewen.blog.csdn.net&#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;本文由 丶布布原创&#xff0c;首发于 CSDN&#xff0c;转载注明出处&#x1f649;&#x1f4e2;现…

【C语言 | 指针】数组参数 和 指针参数

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

IDEA利用插件完成properties与yml的互相转换(mac与wins通用)

步骤一、插件安装 点击屏幕左上方的IDEA&#xff0c;然后点击Preferences(相当于wins里的settings) 进入后点击Plugins&#xff0c;在插件商城中搜索并安装 Convert YAML and Properties File 这个插件 二、使用 右键选择你需要转换的配置文件&#xff0c;选择Convert YAML …

解决高风险代码:Mass Assignment: Insecure Binder Configuration

Abstract 用于将 HTTP 请求参数绑定到模型类的框架绑定器未显式配置为允许或禁止特定属性 Explanation 为便于开发和提高生产率&#xff0c;现代框架允许自动实例化一个对象&#xff0c;并使用名称与要绑定的类的属性相匹配的 HTTP 请求参数填充该对象。对象的自动实例化和填充…

安装Anaconda和pytorch

首先看下自己电脑是否有英伟达的显卡&#xff0c;如果有的话可以安装GPU版本&#xff0c;没有的话可以安装CPU版本。 CPU版本 1.安装Anaconda 首先去官网下载Anaconda。 点击download&#xff0c;下载的就是最新版本的。 下载完成后&#xff0c;直接运行下步就行 注意到路径…

Chrome更新

Chrome无法通过360软件管家升级&#xff0c;最方便的升级方法应该是通过Chrome本身进行升级&#xff0c;但可能需要自备梯子。 点击Chrome右上角三个点 点击帮助 点击关于Google Chrome 在弹出的页面中查看是否是最新版本&#xff0c;如果不是最新版本会有一个升级的按钮&a…

开题PPT答辩复盘

目录 总体思路加粗和红体字使用研究现状之后主要研究内容讨论 总体思路 分为五个部分&#xff0c;规定在10分钟以内讲完。这次开题答辩&#xff0c;主要是要讲清楚研究背景和意义&#xff0c;国内外研究现状。因此前两部分需要花大概6分钟重点解释&#xff0c;主要研究内容用2…

【Linux】进程周边003之进程优先级

&#x1f440;樊梓慕&#xff1a;个人主页 &#x1f3a5;个人专栏&#xff1a;《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C》《Linux》 &#x1f31d;每一个不曾起舞的日子&#xff0c;都是对生命的辜负 目录 前言 1.基本概念 2.PRI与NI 3.利…

使用护眼台灯能不能有效预防近视?分享高品质的护眼台灯

在近几年&#xff0c;儿童青少年近视率非常高。很多家长认为孩子近视的原因是没有养成正确的用眼习惯&#xff0c;例如经常趴桌子写作业、眯眼看书等&#xff0c;但实际上这些坏习惯是因为没有合适的光线而导致的。所以安排一盏合适的台灯给孩子学习是非常重要的。 目前而言最好…

图片压缩软件4K Image Compressor Pro mac特点介绍

4K Image Compressor Pro mac是一款专业的图片压缩软件&#xff0c;它可以在不损失图像质量的前提下&#xff0c;优化图片文件的大小&#xff0c;从而节省存储空间&#xff0c;方便在社交媒体上共享图片&#xff0c;并优化网站加载速度。 4K Image Compressor Pro mac软件特点 …

29、Windows安全配置

文章目录 一、Windows安全配置简介二、账户策略2.1 密码策略2.2 账户锁定策略 三、本地策略3.1 用户权限分配 四、安全设置4.1 账户4.2 审核4.3 设备4.4交互式登录4.5 网络访问4.6 网络安全4.7 用户账户控制4.8 防火墙配置 五、高级审核策略设置5.1 账户登录5.2 账户管理5.3 对…

MDM主数据平台如何实现质量管控

当企业业务以及信息化建设发展到一定阶段后&#xff0c;都不可避免地要进行信息化和数据的治理工作&#xff0c;而主数据治理则是数据治理的基础。随着信息系统和业务的增加&#xff0c;系统打通与数据集成共享必然会成为企业信息化建设的瓶颈&#xff0c;而要实现系统集成&…

怪兽吃糖果

欢迎来到程序小院 怪兽吃糖果 玩法&#xff1a;左右飞出的糖果&#xff0c;点击鼠标糖果即为怪兽吃掉&#xff0c;不同的糖果不同的分数奖励&#xff0c; 吃不掉的糖果会扣除一次生命&#xff0c;共三次生命值&#xff0c;点击炸弹游戏结束&#xff0c;快去吃糖果吧^^开始游戏…

Pyhon基于YOLOV实现的车辆品牌及型号检测项目源码+模型+项目文档

项目运行运行录屏&#xff1a; Pyhon基于YOLOV实现的车辆品牌及型号检测项目运行录屏 完整代码下载地址&#xff1a;Pyhon基于YOLOV实现的车辆品牌及型号检测项目 项目背景&#xff1a; 车辆检测及型号识别广泛应用于物业&#xff0c;交通等的管理场景中。通过在停车场出入口…

在Spring Cloud中实现Feign声明式服务调用客户端

如果你学过Spring Cloud&#xff0c;你应该知道我们可以通过OpenFeign从一个服务中调用另一个服务&#xff0c;我们一般采用的方式就是定义一个Feign接口并使用FeignClient注解来进行标注&#xff0c;feign会默认为我们创建的接口生成一个代理对象。 当我们在代码中调用Feign接…

Leetcode143 重排链表

重排链表 题解1 线性表 给定一个单链表 L 的头节点 head &#xff0c;单链表 L 表示为&#xff1a; L0 → L1 → … → Ln - 1 → Ln请将其重新排列后变为&#xff1a; L0 → Ln → L1 → Ln - 1 → L2 → Ln - 2 → …不能只是单纯的改变节点内部的值&#xff0c;而是需要实际…

【Week P1】 MNIST手写数字识别

文章目录 一、环境配置1.1 安装环境1.2 设置环境&#xff0c;开始本文内容 二、准备数据三、搭建网络结构四、开始训练五、查看训练结果六、总结2.1 ⭐ torchvision.datasets.MNIST详解(Line4 & Line9)2.2 ⭐ torch.utils.data.DataLoader详解(Line4 & Line9)2.3 ⭐ sq…
最新文章