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

news2024/11/15 23:37:32

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

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

条件变量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;
}

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

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

相关文章

电路 - 笔记2

1 555 芯片 2 类比 - pU*I 与 Fm*a 是不是可以与牛顿定律类比 - Fm*a 人的力量&#xff08;F&#xff09;有限。 当推大箱子&#xff08;m&#xff09;时&#xff0c;加速度&#xff08;a&#xff09;就不会很大 当推小箱子&#xff08;m&#xff09;时&#xff0c;加速度…

RTE 大会报名丨AI 时代新基建:云边端架构和 AI Infra ,RTE2024 技术专场第二弹!

所有 AI Infra 都在探寻规格和性能的最佳平衡&#xff0c;如何构建高可用的云边端协同架构&#xff1f; 语音 AI 实现 human-like 的最后一步是什么&#xff1f; AI 视频的爆炸增长&#xff0c;给新一代编解码技术提出了什么新挑战&#xff1f; 当大模型进化到实时多模态&am…

mysql批量修改表前缀

现有表前缀xh,批量修改为fax_需要怎么做 SELECTCONCAT(ALTER TABLE ,table_name, RENAME TO fax_,substring(table_name, 3),;) FROMinformation_schema. TABLES WHEREtable_name LIKE xh_%; 运行之后可以但是生成了一批修改表明的命令 此时批量复制执行就可实现批量修改表前…

架构设计读后有感——原则

成为架构时是程序员的梦想&#xff0c;并不意味着把编程做好就能够自然的成为一个架构师&#xff0c;他们之间有一个鸿沟->“不确定性” 不确定性&#xff1a;编程本质上说是不存在不确定性的&#xff0c;因为一个输入可以通过逻辑的运算得到确定的值&#xff0c;即使是机器…

电脑ip变了后导致原来的虚拟机静态ip失效问题

电脑ip变了后导致原来的虚拟机静态失效问题处理 静态IP设置的几个要点 查看本地网络的配置 VMware虚拟机网络设置 子网IP必须和本地ip在同一个网段下&#xff0c;本地的ip是192.168.1.10&#xff0c;那我子网ip就应该是192.168.1.xxx&#xff0c; 网关ip需要保持一致。 DHCP…

剧本杀分类管理

1用户界面 用户注册/登录模块与剧本杀填写模块&#xff1a;用户注册/登录成功后&#xff0c;将用户信息传递给剧本杀填写模块&#xff0c;以便用户进行剧本杀的填写。 图6-1登录注册 剧本杀填写模块与剧本杀分类管理模块&#xff1a;剧本杀填写完成后&#xff0c;将剧本杀信息…

Adaptive Object Detection with Dual Multi-Label Prediction

gradient reversal layer (GRL) 辅助信息 作者未提供代码

小孩真的需要手机上学吗?怎样远程了解他在学校用iPhone干什么?

互联网在教育、娱乐、社交等多个领域发挥着重要作用&#xff0c;成为孩子们获取知识、放松娱乐的重要渠道。孩子们首次接触互联网的年龄越来越小&#xff0c;有的甚至在幼儿园阶段就开始使用智能设备。 一些学校推行电子书包、网络作业等&#xff0c;虽然提高了效率&#xff0c…

【Golang】Go语言中如何面向对象?

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

Vue3:toRaw与markRaw

目录 一.toRaw 1.性质 2.作用 二.markRaw 1.性质 2.作用 三.toRaw的使用 四.markRaw的使用 五.代码示例 在Vue 3中&#xff0c;toRaw和markRaw是两个用于处理响应式对象的全局函数。 一.toRaw 1.性质 toRaw是一个全局函数&#xff0c;它接受一个由reactive或ref生成…

JS面试真题 part6

JS面试真题 part6 26、如何判断一个元素是否在可视区域中27、什么是单点登录&#xff1f;如何实现28、 如何实现上拉加载&#xff0c;下拉刷新29、说说你对正则表达式的理解&#xff1f;应用场景&#xff1f;30、说说你对函数式编程的理解&#xff1f;优缺点 26、如何判断一个元…

Vue74 路由的props配置

笔记 ​ 作用&#xff1a;让路由组件更方便的收到参数 {name:xiangqing,path:detail/:id,component:Detail,//第一种写法&#xff1a;props值为对象&#xff0c;该对象中所有的key-value的组合最终都会通过props传给Detail组件// props:{a:900}//第二种写法&#xff1a;props…

《动手学深度学习》笔记1.11——实战Kaggle比赛:预测房价+详细代码讲解

目录 0. 前言 原书正文 1. 下载和缓存数据集 1.1 download() 下载数据集 1.2 download_extract() 解压缩 2. Kaggle 简介 3. 访问和读取数据集 4. 数据预处理 5. 训练&#xff08;核心难点&#xff09; 5.1 get_net() 定义模型-线性回归 5.2 log_rmse() 对数均方根…

见合八方亮相重庆光纤传感大会(OFS China-2024)

2024年9月20日至22日&#xff0c;第十二届中国光纤传感大会&#xff08;OFS China-2024&#xff09;在重庆成功举办&#xff0c;该大会旨在展示光纤传感技术在多个领域的最新研究成果&#xff0c;推动该技术的产业化进程。 在本次大会上&#xff0c;天津见合八方光电科技有限公…

JDK1.8与JDK17相互切换

&#x1f4d6; 前言&#xff1a;在电脑已经安装jdk17后&#xff0c;发现有些项目不兼容&#xff0c;需要用到以前的jdk1.8&#xff0c;本文介绍简单切换的方法。 &#x1f50e; 甲骨文jdk1.8官网下载 下载完jdk1.8后&#xff0c;可以将其与jdk17放在同一目录层级下。 搜索栏直…

我眼中的Token2049 是一场加密大秀

今年Token2049&#xff0c;其实我也收到很多来自币圈朋友、项目方或交易所的邀请&#xff0c;都一一婉拒了。因为每年9月&#xff0c;都是我一年来最忙碌的日子。一方面进入金九银十的销售旺季&#xff0c;另外副业也需要谈一些团购业务。 我喜欢Web3&#xff0c;也曾是 #Bitg…

《操作系统 - 清华大学》1 -1:操作系统概述 —— 内容概述

文章目录 1. 内容摘要2. 实验内容 1. 内容摘要 在这里对学习内容做一个整体上的介绍&#xff0c;那在这里包括我们要学习的内容&#xff0c;实验的内容。操作系统课涉及到计算机系统当中的资源管理&#xff0c;所以我们围绕着操作系统的实现来介绍相关内容&#xff0c;那主要分…

Java之路--瓦解逻辑控制与方法使用已是瓮中捉鳖

嗨嗨大家&#xff01;今天我们来学习逻辑运算和方法的使用~ 目录 一 逻辑控制 1 分支结构 1.1 if语句 1.2 switch 语句 2 循环结构 2.1 while 循环 2.2 for 循环 2.3 do while 循环 2.4 break 2.5 continue 3. 输出输入 二、方法的使用 1 方法定义语法 2 实参和…

NCU-机器学习-作业3:RANK: 0.0代码(No)

想体验一把No.1的快乐吗&#xff1f;话不多说直接上代码。 代码&#xff1a; import osimport pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC import numpy as npdef get_dataset(path):dataset, labels [], []filenames os…

SDK使用指南

本文档主要讲解音视频终端 SDK 产品家族下各子产品的能力支持及购买方式&#xff0c;并提供了相关 Demo 和 SDK 集成资源&#xff0c;具体体验步骤如下&#xff1a; 步骤一&#xff1a;了解产品 1.1 音视频终端 SDK 产品家族介绍 音视频终端 SDK&#xff08;腾讯云视立方&…