【线程】线程的同步---生产消费者模型

本文重点:理解条件变量和生产者消费者模型

同步是在保证数据安全的情况下,让我们的线程访问资源具有一定的顺序性

条件变量cond

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了,就是申请锁失败了,它就要添加到一个队列中等待,这个队列叫做等待队列,把这个等待队列可以叫做条件变量,也就是说申请锁失败,进行等待是在条件变量下等的,这样它们排好队,就有顺序性。

条件变量依赖于锁的实现

那我怎么知道几时要在条件变量下等呢?

一定是临界资源不就绪,没错,临界资源也是有状态的!!

你怎么知道临界资源是就绪还是不就绪的?

你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!

条件变量接口

初始化接口和锁是一样的

pthread_cond_t: 系统提供的类型

cond:要初始化的条件变量
attr:条件变量的属性,设为nullptr

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
全局的条件变量,自动初始化和销毁

等待条件满足

cond:要在这个条件变量上等待
mutex:锁,后面详细解释 

 唤醒等待

signal是唤醒一个线程,broadcast是唤醒条件变量下的全部线程 

下面通过代码来理解上面的问题

注意函数接口的位置,想想为什么函数是在这?

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

using namespace std;

int cnt=0;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

void* Count(void* args)
{
    pthread_detach(pthread_self());//分离线程
    uint64_t number=(uint64_t)args;
    cout<<"pthread: "<<number<<",create success"<<endl;
    while(1)
    {
        pthread_mutex_lock(&mutex);
        // 我们怎么知道我们要让一个线程去休眠了那?一定是临界资源不就绪,没错,临界资源也是有状态的!!
        // 你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!
        pthread_cond_wait(&cond,&mutex);
        //为什么这个函数在这里?因为临界资源不就绪,就要等待
        //为什么还要一个锁的参数?因为这个线程在前面的时候申请了锁,此时线程持有锁
        //1.pthread_cond_wait让线程等待的时候,会自动释放锁!2.唤醒而返回的时候,重新持有锁
        cout<<"pthread: "<<number<<", cnt: "<<cnt++<<endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
    return nullptr;
}
int main()
{
    for(uint64_t i=0;i<5;i++)
    {
        pthread_t tid;
        pthread_create(&tid,nullptr,Count,(void*)i);
        usleep(1000);
    }
    //让主线程把等待的线程唤醒
    while(1)
    {
        sleep(1);
        pthread_cond_signal(&cond);//唤醒条件变量下的一个线程,默认第一个开始
        cout<<"signal one thread..."<<endl;

    }
    return 0;
}

 

从结果看出,线程访问临界资源时具有一定的顺序性 

要点: 

pthread_cond_wait函数的位置

我们怎么知道我们要让一个线程去休眠了那?

一定是临界资源不就绪,没错,临界资源也是有状态的!!

你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!

为什么这个函数在这里?因为临界资源不就绪,就要等待

为什么还要一个锁的参数?因为这个线程在前面的时候申请了锁,此时线程持有锁

1.pthread_cond_wait让线程等待的时候,会自动释放锁!2.唤醒而返回的时候,重新持有锁

总的来说:条件变量依赖于锁的实现

可能大家还没有很理解,那就在看看下面的生产者消费者模型,再次理解线程怎么等待和唤醒的

生产者消费者模型的原理

producter  consumer

生产者生产产品供货给超市,消费者通过从超市消费,得到产品,超市就相当于一个大的缓存,生产者生产的产品都可以放在超市里,供消费者使用,这样可以提高效率,支持忙闲不均,生产和消费的行为存在一定的解耦(解耦的意思也就是生产者生产是一个执行流,消费者消费是另一个执行流,两个执行流可以同时访问超市资源,就有一定的解耦,不像只有一个执行流时,就不能同时执行)

在生产者生产产品的过程中,消费者是不能访问产品的,但是消费者可以处理产品,相反,在消费者访问产品时,生产者无法生产,但是生产者可以获取数据,这样结合,效率就会很高

记住生产者消费者模型就记住“321”原则

3种关系

2种角色--生产者,消费者

1个场所--超市

生产者消费者模型的实现

思路:创建两个线程,单生产单消费的,一个充当消费者,一个充当生产者,消费者消费任务,生产者生产任务。写一个等待队列的类,方法写生产任务和消费任务,这个是共享资源,需要加锁和解锁,看资源状态来添加条件变量,在写一个任务的类,写清楚是什么任务

大家可以把下面的三个文件的代码拷贝到自己的Visual Studio Code下看,更好看一些,代码是不难得,认真看是能看懂的

一定要特别关注条件变量的接口的使用和他的位置

main.cc

#include "blockqueue.hpp"
#include "task.hpp"
#include<time.h>

using namespace std;

void *Producter(void *p_args)
{
    int len=opers.size();
    BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(p_args);
    while (1)
    {
        // 模拟生产者获取数据的过程
        int data1=rand()%10+1;//[1,10]
        usleep(100);
        int data2=rand()%10;
        char op=opers[rand()%len];
        Task t(data1,data2,op);
        bq->push(t);//生产任务
        // cout << "生产了一个任务:" << t.GetTask() <<endl;
        printf("生产了一个任务: %s\n",t.GetTask().c_str());
        sleep(1);
    }
    return nullptr;
}

void *Consumer(void *c_args)
{
    BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(c_args);
    while (1)
    {
        //消费任务
        Task t= bq->pop();
        //计算
        t.run();//模拟消费者消费消费数据的过程
        // cout << "处理任务: " <<t.GetTask()<<" 运算结果:"<<t.GetResult()<<endl;
        printf("处理任务:%s,运算结果:%s\n",t.GetTask().c_str(),t.GetResult().c_str());
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    pthread_t p, c;
    BlockQueue<Task>* bq=new BlockQueue<Task>;
    pthread_create(&p, nullptr, Producter, (void *)bq);
    pthread_create(&c, nullptr, Consumer, (void *)bq);

    pthread_join(p, nullptr);
    pthread_join(c, nullptr);

    delete bq;

    return 0;
}

blockqueue.hpp

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

using namespace std;

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
//生产者和消费者的条件变量要分开的,它们是不同的
pthread_cond_t c_cond=PTHREAD_COND_INITIALIZER;//消费者的条件变量
pthread_cond_t p_cond=PTHREAD_COND_INITIALIZER;//生产者的条件变量

template<class T>
class BlockQueue
{
public:
    BlockQueue(int maxcap=4):_maxcap(maxcap)
    {}

    T pop()
    {
        //加锁
        pthread_mutex_lock(&mutex);
        // 我们怎么知道我们要让一个线程去休眠了那?一定是临界资源不就绪,没错,临界资源也是有状态的!!
        // 你怎么知道临界资源是就绪还是不就绪的?你判断出来的!判断是访问临界资源吗?必须是的,也就是判断必须在加锁之后!!!
        if(_wait_q.size()==0)//队列为空,临界资源不就绪,就不能消费了,在消费者条件变量下等待
        {
            pthread_cond_wait(&c_cond,&mutex);//1.pthread_cond_wait让线程等待的时候,会自动释放锁!2.唤醒而返回的时候,重新持有锁
        }
        T out=_wait_q.front();
        _wait_q.pop();
        pthread_cond_signal(&p_cond);//消费了一个,生产者肯定可以生产了,唤醒生产者的条件变量下的一个线程
        pthread_mutex_unlock(&mutex);
        return out;
    }
    void push(const T& in)
    {
        pthread_mutex_lock(&mutex);
        if(_wait_q.size()==_maxcap)//队列满了,临界资源不就绪,就不能生产了,在生产者条件变量下等待
        {
            pthread_cond_wait(&p_cond,&mutex);
        }
        _wait_q.push(in);
        pthread_cond_signal(&c_cond);//生产了一个,消费者肯定可以消费了,唤醒消费者的条件变量下的一个线程
        pthread_mutex_unlock(&mutex);
    }
    ~BlockQueue()
    {}

private:
    queue<T> _wait_q;//等待队列
    int _maxcap;//队列的最大容量

};

task.hpp

#pragma once
#include <iostream>
#include <string>

using namespace std;
string opers="+-*/%";

enum
{
    Divzero = 1,
    Modzero,
    Unknown
};

class Task
{
public:
    Task(int data1, int data2, char op) : _data1(data1), _data2(data2), _op(op), _result(0), _exitcode(0)
    {}
    void run()
    {
        switch (_op)
        {
        case '+':
        {
            _result = _data1+_data2;
            break;
        }
        case '-':
        {
            _result = _data1-_data2;
            break;
        }
        case '*':
        {
            _result = _data1*_data2;
            break;
        }
        case '/':
        {
            if (_data2 == 0) _exitcode = Divzero;
            else _result = _data1/_data2;
            break;
        }
        case '%':
        {
            if (_data2 == 0) _exitcode = Modzero;
            else _result = _data1%_data2;
            break;
        }
        default:
        {
            _exitcode=Unknown;
            break;
        }
        }
    }
    void operator()()
    {
        run();
    }
    string GetResult()
    {
        string r=to_string(_data1);
        r+=_op;
        r+=to_string(_data2);
        r+='=';
        r+=to_string(_result);
        r+='[';
        r+=to_string(_exitcode);
        r+=']';
        return r;
    }
    string GetTask()
    {
        string r=to_string(_data1);
        r+=_op;
        r+=to_string(_data2);
        r+="=?";
        return r;
    }

private:
    int _data1;
    int _data2;
    char _op;
    int _result;
    int _exitcode;
};

main.cc中打印用printf比较好,用cout我的平台下会有点乱序

 

看懂上面的实现再来看这个问题

什么是伪唤醒?

假如多个生产者生产了四个产品,生产者条件变量下的等待的线程有三个,消费者消费了一个产品,假如用的pthread_cond_broadcast接口,那就唤醒了那三个线程,它们就pthread_cond_wait函数返回,重新持有锁,就生产产品,但是到第二个线程的时候,队列已经满了,第二个线程还会生产 ,那就越界出错了

怎么防止伪唤醒?

就在条件判断时改为循环,不要if,这样函数返回时还要判断(上面的代码还是if,你们自己改吧) 

下面的代码是把上面的实现改为多生产者多消费者

只改了main.cc

#include "blockqueue.hpp"
#include "task.hpp"
#include<time.h>

using namespace std;

void *Producter(void *p_args)
{
    int len=opers.size();
    BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(p_args);
    while (1)
    {
        // 模拟生产者获取数据的过程
        int data1=rand()%10+1;//[1,10]
        usleep(100);
        int data2=rand()%10;
        char op=opers[rand()%len];
        Task t(data1,data2,op);
        bq->push(t);//生产任务
        // cout << "生产了一个任务:" << t.GetTask() <<endl;
        printf("生产了一个任务:%s,thread id: %x\n",t.GetTask().c_str(),pthread_self());
        sleep(1);
    }
    return nullptr;
}

void *Consumer(void *c_args)
{
    BlockQueue<Task>* bq=static_cast<BlockQueue<Task>*>(c_args);
    while (1)
    {
        //消费任务
        Task t= bq->pop();
        //计算
        t.run();//模拟消费者消费消费数据的过程
        // cout << "处理任务: " <<t.GetTask()<<" 运算结果:"<<t.GetResult()<<endl;
        printf("处理任务:%s,运算结果:%s,thread id: %x\n",t.GetTask().c_str(),t.GetResult().c_str(),pthread_self());
    }
    return nullptr;
}

int main()
{
    srand(time(nullptr));
    pthread_t p[5], c[3];
    BlockQueue<Task>* bq=new BlockQueue<Task>;
    for(int i=0;i<5;i++)
    pthread_create(p+i, nullptr, Producter, (void *)bq);
    for(int i=0;i<3;i++)
    pthread_create(c+i, nullptr, Consumer, (void *)bq);

    for(int i=0;i<5;i++)
    pthread_join(p[i], nullptr);
    for(int i=0;i<3;i++)
    pthread_join(c[i], nullptr);

    delete bq;

    return 0;
}

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

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

相关文章

Vue中集中常见的布局方式

布局叠加 完整代码最外层的Container设置为relative&#xff0c;内部的几个box设置为absolute <template><div class"container"><div class"box box1">Box 1</div><div class"box box2">Box 2</div><d…

LeetCode讲解篇之53. 最大子数组和

文章目录 题目描述题解思路题解代码 题目描述 题解思路 该问题我们可以转换为求以i为最后一个元素的0 ~ i范围内的最大子数组和&#xff0c;然后其中的所有的最大子数组和的最大值就是我们要返回的答案 题解代码 func maxSubArray(nums []int) int {ans : nums[0]for i : 1;…

CLIP模型微调简明指南

CLIP 等多模态模型通过将图像等复杂对象与易于理解、生成和解析的文本描述联系起来&#xff0c;开辟了新的 AI 用例。但是&#xff0c;像 CLIP 这样的现成模型可能无法代表特定领域中常见的数据&#xff0c;在这种情况下&#xff0c;可能需要进行微调以使模型适应该领域。 这篇…

8.使用 VSCode 过程中的英语积累 - Help 菜单(每一次重点积累 5 个单词)

前言 学习可以不局限于传统的书籍和课堂&#xff0c;各种生活的元素也都可以做为我们的学习对象&#xff0c;本文将利用 VSCode 页面上的各种英文元素来做英语的积累&#xff0c;如此做有 3 大利 这些软件在我们工作中是时时刻刻接触的&#xff0c;借此做英语积累再合适不过&a…

【Java】虚拟机(JVM)内存模型全解析

目录 一、运行时数据区域划分 版本的差异&#xff1a; 二、程序计数器 程序计数器主要作用 三、Java虚拟机 1. 虚拟机运行原理 2. 活动栈被弹出的方式 3. 虚拟机栈可能产生的错误 4. 虚拟机栈的大小 四、本地方法栈 五、堆 1. 堆区的组成&#xff1a;新生代老生代 …

计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-22

计算机前沿技术-人工智能算法-大语言模型-最新论文阅读-2024-09-22 引言: 全球最热销的国产游戏-《黑神话: 悟空》不仅给世界各地玩家们带来愉悦&#xff0c;而且对计算机人工智能研究也带来新的思考。在本期的论文速读中&#xff0c;我们带来一篇关于视觉语言模型&#xff0…

深度解析与解决方案:U盘有盘符但无法打开的困境

引言&#xff1a;U盘困境初现 在日常工作与生活中&#xff0c;U盘作为便携式存储设备&#xff0c;扮演着数据传输与备份的重要角色。然而&#xff0c;不少用户会遇到这样一个棘手问题&#xff1a;U盘在插入电脑后能够正常显示盘符&#xff0c;但尝试打开时却遭遇拒绝访问或提示…

运维,36岁,正在经历中年危机,零基础入门到精通,收藏这一篇就够了

我今年36岁&#xff0c;运维经理&#xff0c;985硕士毕业&#xff0c;目前正在经历中年危机&#xff0c;真的很焦虑&#xff0c;对未来充满担忧。不知道这样的日子还会持续多久&#xff0c;突然很想把这些年的经历记录下来&#xff0c;那就从今天开始吧。 先说一下我的中年危机…

华为LTC流程架构分享

文末附LTC流程管理PPT下载链接~ 前面笔者分享了华为LTC流程相关PPT&#xff0c;应读者需求&#xff0c;今天从架构角度进行再次与读者共同学习下LTC流程架构。 华为LTC流程架构是一个全面且集成的业务流程体系&#xff0c;从线索发现开始&#xff0c;直至收回现金&#xff0c…

浅谈Agent智能体

Agent智能体无疑是24年最为火爆的话题之一&#xff0c;那么什么是Agent智能体&#xff1f;有什么作用&#xff1f;为什么需要Agent智能体&#xff1f; 用下边一张图简单说明一下 每日进步一点点

气膜健身馆:提升运动体验与健康的理想选择—轻空间

近年来&#xff0c;气膜健身馆作为一种新兴的运动场所&#xff0c;正逐渐受到越来越多健身爱好者的青睐。这种独特的建筑形式不仅提供了良好的运动环境&#xff0c;更在健康和运动表现上展现出诸多优势。 优越的空气质量 气膜结构的核心技术通过内外气压差形成稳定的气膜&#…

C++ 9.27

作业&#xff1a; 将之前实现的顺序表、栈、队列都更改成模板类 Stack #include <iostream> using namespace std; template <typename T> class Stack { private: T* arr; // 存储栈元素的数组 int top; // 栈顶索引 int capacity; // 栈的…

【高频SQL基础50题】6-10

目录 1.上级经理已离职的公司员工 2.修复表中的名字 3. 寻找用户推荐人 4.产品销售分析 I 5.平均售价 1.上级经理已离职的公司员工 子查询。 先根据薪水大小查询&#xff0c;再根据manager_id查询该员工是否存在&#xff0c;最后做排序。 # Write your MySQL query st…

Proteus-7.8sp2安装

目录 一、D盘新建空文件夹&#xff0c;名为Proteus。 二、安装软件 三、破解 四、汉化 五、卸载软件 一、D盘新建空文件夹&#xff0c;名为Proteus。 二、安装软件 1.双击P7.8sp2.exe 2.next 三、破解 1.双击 Proteus Pro 7.8 SP2破解 1.0.exe 2. 升级 打开软件&#x…

网站建设中,营销型网站与普通网站有什么区别

营销型网站与普通网站在建站目的、交互设计以及结构优化等方面存在区别。以下是具体分析&#xff1a; 建站目的 营销型网站&#xff1a;以销售和转化为主要目标&#xff0c;通过专业的市场分析和策划来吸引潜在客户&#xff0c;并促使其采取购买行动。普通网站&#xff1a;通常…

8610 顺序查找

### 思路 1. **创建顺序表**&#xff1a;从输入中读取元素个数和元素值&#xff0c;构造顺序表。 2. **顺序查找**&#xff1a;在顺序表中依次查找关键字&#xff0c;找到则返回位置&#xff0c;否则返回0。 ### 伪代码 1. **创建顺序表**&#xff1a; - 动态分配存储空间。…

C. Cards Partition 【Codeforces Round 975 (Div. 2)】

C. Cards Partition 思路&#xff1a; 可以O(n)直接判断&#xff0c;牌组从大到小依次遍历即可。 不要用二分答案&#xff0c;因为答案不一定是单调的 代码: #include <bits/stdc.h> #define endl \n #define int long long #define pb push_back #define pii pair<…

Verilog基础:时序调度中的竞争(四)(描述时序逻辑时使用非阻塞赋值)

相关阅读 Verilog基础https://blog.csdn.net/weixin_45791458/category_12263729.html?spm1001.2014.3001.5482 作为一个硬件描述语言&#xff0c;Verilog HDL常常需要使用语句描述并行执行的电路&#xff0c;但其实在仿真器的底层&#xff0c;这些并行执行的语句是有先后顺序…

重头开始嵌入式第四十四天(硬件 ARM裸机开发)

目录 裸机开发 一、开发背景 二、开发特点 三、开发流程 四、应用领域 使用的软件硬件 软件&#xff1a;keil 硬件&#xff1a;三星S3C2440A JTAG 开发原理 ​编辑 开发步骤 ​编辑 点亮小灯 按键控制亮灭 裸机开发 ARM 裸机开发是指在没有操作系统的情况…

信号处理: Block Pending Handler 与 SIGKILL/SIGSTOP 实验

1. 信号处理机制的 “三张表” kill -l &#xff1a;前 31 个信号为系统标准信号。 block pending handler 三张表保存在每个进程的进程控制块 —— pcb 中&#xff0c;它们分别对应了某一信号的阻塞状态、待处理状态以及处理方式。 block &#xff1a;通过 sigset_t 类型实现&…