【1++的Linux】之线程(三)含生产者消费者模型

👍作者主页:进击的1++
🤩 专栏链接:【1++的Linux】

文章目录

  • 一,可重入与线程安全
  • 二,死锁
  • 三,线程同步
    • 什么是线程同步?
    • 怎么实现线程同步
    • 条件变量
  • 四,生产者与消费者模型
    • 1,生产者与消费者模型的基本组成及其概念

一,可重入与线程安全

线程安全: 多个执行流在执行同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

常见的线程不安全情况:

  1. 不保护共享变量的函数
  2. 函数状态随着被调用,状态发生变化的函数
  3. 返回指向静态变量指针的函数
  4. 调用线程不安全函数的函数

常见的线程安全的情况:

  1. 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  2. 类或者接口对于线程来说都是原子操作
  3. 多个线程之间的切换不会导致该接口的执行结果存在二义性

常见不可入的情况:

  1. 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  2. 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  3. 可重入函数体内使用了静态的数据结构

常见的可入情况:

  1. 不使用全局变量或静态变量
  2. 不使用用malloc或者new开辟出的空间
  3. 不调用不可重入函数
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的联系与区别:

联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

线程安全描绘的是线程之间互相影响的一种状态或者可能性。而重入描述的是一个函数可不可以被重复进入。

在这里插入图片描述

这个insert是加锁的,所以多个线程访问的时候是安全的,比如main函数里insert执行到第二句的时候,信号来了,导致它处理信号去了,但是main函数执行流是申请锁了,它抱着锁,信号递达的时候执行信号捕捉的方法,执行handler,handler里面也有一个insert,那么insert就重入了,可是insert函数进来的时候,信号捕捉执行流要进行申请锁。此时就出现,主线程申请锁成功了正在访问临界资源,然后信号来了,执行了信号处理函数,此时又进行申请锁了,也就是说同一个进程申请了两次锁,第一次我成功申请了锁,第二次我又去申请锁,但是锁没了(其实是被你自己申请了),此时你就被挂起了。可最尴尬的是你是抱着锁被挂起的,你在等别人释放锁唤醒你,可是锁被你拿着呢,没有人释放,也就没有人唤醒。所以你这个进程就被永远的挂起了,这就叫做一个线程是安全的,但不一定是可重入的。

在这里插入图片描述

二,死锁

什么是死锁:

死锁是指在一组执行流中的各个执行流均占有不会释放的资源,但因互相申请被其他执行流所站用的不会释放的资源而处于的一种永久等待状态。

死锁的条件:

互斥条件:一个资源每次只能被一个执行流使用。
请求与保持条件:一个执行流因请求资源而阻塞时,并且对已获得的资源保持不放。
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

那么该如何避免死锁呢?

  1. 破坏死锁的四个必要条件
  2. 加锁顺序一致
  3. 避免锁未释放的场景
  4. 资源一次性分配

避免死锁的算法:
死锁检测算法
银行家算法

三,线程同步

什么是线程同步?

在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

怎么实现线程同步

实现线程同步即实现怎么能够让线程按照某种特定的顺序去访问临界资源?
我们用条件变量来实现。条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

就像我们去手机店买手机,发现想要的那款手机没有货,所以我们没有买,回去了,第二天又来问,还是没有,第三天,第四天。。。你连续一个月每天都去问,有没有货,这样是不是浪费了你的时间,**(做法没错,但不合理)**但如果你将导购的微信加上,等有货时,他微信通知你,你再去买,这样是不是就方便的多。

一般而言,因为有锁的缘故,我们比较困难去了解资源的情况(判断资源是否满足,也是访问资源的过程),这样让一方通知另一方资源已就绪的场景就是条件变量。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

为什么要有线程同步呢?
主要是为了解决访问临界资源合理性的问题。不造成别人的饥饿问题和自身资源的浪费。

条件变量

当我们申请临界资源时,先要做资源是否存在的判断,那么对资源的判断也是对资源进行访问的一种,因此对资源的判断也要在加锁和解锁之间。常规的检测方式注定了我们要进行频繁的申请和释放锁,有没有办法让我们的线程在检测到资源不就绪时就不在频繁的去自检,而是去等待通知,等条件就绪的时候再去唤醒呢?—这种方式就是我们的条件变量。

条件变量的使用:
在这里插入图片描述
条件变量的使用与互斥量的使用大同小异,都可以进行直接用宏初始化或者调用初始化函数进行初始化。

在这里插入图片描述
pthread_cond_wait函数是,当由于条件不满足而调用它时,该执行流将会进行阻塞式等待,而且还会将锁打开,直到收到唤醒的信号时,会再次申请锁,并从阻塞时的位置继续向后执行。

pthread_cond_timedwait 函数与上述函数不同的是其比pthread_cond_wait多了一个时间参数,表示历经多长时间后,即使每被唤醒也解除阻塞。

这个函数和pthread_ cond_ wait主要差别在于第三个参数,这个abstime,从函数的说明来
看,这个参数并不是像红字所描述的经历了abstime段时间后,而是到达了abstime时间,后才解锁,所以这里当我们用参数的时候不能直接就写个时间间隔,比如5S,而是应该写上到达的时间点所以初始化的过程为:

struct timespec timeout;
//定义时间点
timeout.tv_ sec= time(0)+ 1; //time(0)代表的是当前时间而//tv_ sec 是指的是秒
timeout.tv_ nsec=0;
//tv_ nsec代表的是纳秒时间

在这里插入图片描述

pthread_cond_signal
函数的作用是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回。
使用pthread_cond_signal一般不会有“惊群现象”产生,他最多只给一个线程发信号。假如有多个线程正在阻塞等待着这个条件变量的话,那么是**根据各等待线程优先级的高低确定哪个线程接收到信号开始继续执行。**如果各线程优先级相同,则根据等待时间的长短来确定哪个线程获得信号。但无论如何一个pthread_cond_signal调用最多发信一次。(其本质就是将接收到信号的状态由S状态改为R状态)
而pthread_cond_broadcast会给所有阻塞在这个条件变量下的线程发信号。

下面我们展示一段相关代码,以便对上述结论有更深的理解:

//定义全局的锁和条件变量
pthread_mutex_t mtx=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
pthread_cond_t ct=PTHREAD_COND_INITIALIZER;int i=0;
int j=0;
void* ctr(void* argv)
{while(true){cout<<(++j)<<"唤醒----"<<endl;pthread_cond_signal(&ct);sleep(1);}
}void* work(void* argv)
{while(true){pthread_mutex_lock(&mtx);pthread_cond_wait(&ct,&mtx);cout<<++i<<" doing------"<<endl;pthread_mutex_unlock(&mtx);}}
int main()
{pthread_t boss;pthread_t staff[3];pthread_create(&boss,nullptr,ctr,nullptr);for(int i=0;i<3;i++){pthread_create(staff+i,nullptr,work,nullptr);}pthread_join(boss,nullptr);for(int i=0;i<3;i++){pthread_join(staff[i],nullptr);}return 0;
}

在这里插入图片描述

四,生产者与消费者模型

1,生产者与消费者模型的基本组成及其概念

基本组成: 生产者,消费者,交易场所。

基于Blockingqueue的生产者消费者模型: 在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)。

我们以购物为例:
我们将生产者比作生产产品的工厂,将消费者比作购物的人,交易场所则是商场。我们的数据比作商品。

为什么要有超市的存在:其本质是作为商品的缓冲区(暂存商品),从而提高效率。

设计的核心思想:尽可能减少代码耦合,如果发现代码耦合,就要采取解耦技术。让数据模型,业务逻辑和视图显示三层之间彼此降低耦合,把关联依赖降到最低,而不至于牵一发而动全身。超市的存在也是解耦的一种手段。

那么他们三个之间都存在什么样的关系呢?

消费者和消费者之间:互斥,比如强演唱会的票
生产者和生产者 : 互斥,比如商战。
生产者和消费者:存在互斥的关系,比如向消费者想要拿货架中
的商品,而生产者也想往货架中方商品,此时就有了谁先谁后的问题;同步关系,工厂生产出来商品后,顾客才能进行购买,顾客购买后,商品不足时,工厂才会进行生产。

下面我们用相关代码来模拟这一过程:

ypedef std::function<int(int,int)> func_t;class Operat{public:inline static int Add(int x,int y){return x+y;}inline static int Mult(int x,int y){return x*y;}inline static int Sub(int x,int y){return x-y;}};pthread_t tid[3];
func_t func[3]={Operat::Add,Operat::Mult,Operat::Sub};template<class T>
class Blockqueue
{bool IsEmpty(){return dp.size()==0;}bool IsFill(){return dp.size()==cap;}
public:Blockqueue(){pthread_mutex_init(&mtx,nullptr);pthread_cond_init(&EMPTY,nullptr);pthread_cond_init(&Fill,nullptr);}~Blockqueue(){pthread_mutex_destroy(&mtx);pthread_cond_destroy(&EMPTY);pthread_cond_destroy(&Fill);}void Push(T& in){//制作任务int n= pthread_mutex_lock(&mtx);assert(n==0);while(IsFill()){std::cout<<"生产者等待"<<std::endl;pthread_cond_wait(&Fill,&mtx);}std::cout<<"制作任务中"<<std::endl;dp.push(in);pthread_mutex_unlock(&mtx);pthread_cond_signal(&EMPTY);}void Pop(T* out){//消费int n= pthread_mutex_lock(&mtx);assert(n==0);// // 当我被唤醒时,我从哪里醒来呢??从哪里阻塞挂起,就从哪里唤醒, 被唤醒的时候,我们还是在临界区被唤醒的啊// // 当我们被唤醒的时候,pthread_cond_wait,会自动帮助我们线程获取锁// // pthread_cond_wait: 但是只要是一个函数,**就可能调用失败**// // pthread_cond_wait: 可能存在 ** 伪唤醒 的情况**while(IsEmpty())  {pthread_cond_wait(&EMPTY,&mtx);std::cout<<"消费者等待"<<std::endl;}std::cout<<"拿到任务"<<std::endl;*out=dp.front();dp.pop();pthread_mutex_unlock(&mtx);pthread_cond_signal(&Fill);}private:std::queue<T> dp;int cap=4;pthread_mutex_t mtx;pthread_cond_t EMPTY;pthread_cond_t Fill;};
void* productor(void* argv)
{Blockqueue<func_t>* b=(Blockqueue<func_t>*)argv;//必须用指针接收,否则拷贝构造会产生一个新的对象,//导致有一把新锁产生while(true){int n=rand()%3;b->Push(func[n]);sleep(1);}return nullptr;
}void* consumer(void* argv)
{Blockqueue<func_t>* b=(Blockqueue<func_t>*)argv;while(true){func_t ret;b->Pop(&ret);int x=rand()%6;int y=rand()%7;std::cout<<"结果"<<x<<"--"<<y<<"="<<ret(x,y)<<std::endl;sleep(1);}return nullptr;
}int main()
{srand((unsigned)time(nullptr));Blockqueue<func_t>* p_blockq=new Blockqueue<func_t>;pthread_t c[2],p[2];pthread_create(c,nullptr,consumer,p_blockq);pthread_create(c+1,nullptr,consumer,p_blockq);pthread_create(p,nullptr,productor,p_blockq);pthread_create(p+1,nullptr,productor,p_blockq);pthread_join(c[0],nullptr);pthread_join(c[1],nullptr);pthread_join(p[0],nullptr);pthread_join(p[1],nullptr);delete p_blockq;return 0;
}

在这里插入图片描述
下面我们用一张图来形象的展示代码所代表的意思:

在这里插入图片描述
接下来请看VCR: 当我们的顾客去消费时,发现没有要买的商品,此时顾客就会通知工厂并回到家等通知,工厂接到通知后开始生产,将生产好多商品送到超市后,通知顾客来买,此时顾客就可以购物了,若工厂想要在超市的货还有但未满的情况下继续补货,此时要是有顾客来购物,他们就需要进行竞争,(我先买东西还是你先补货)若是在只有没货的情况下进行补货,且工厂生产较快,那么在顾客购物的这段期间,我工厂就可以专心我的货的制造,这是不是就提高了效率;反过来,我消费过快,工厂长在进行补货的时候,我是不是就可以去用我所买的东西,这是不是也提高了效率。

超市就像临界资源一样,我们的生产者想要访问,消费者也想要访问,为了不会因为时序问题而导致数据发生错误,我们只允许一个执行流进入超市,因此消费者和消费者,生产者和生产者,消费者和生产者都有竞争关系。

我们再来看看互斥与同步的关系: 我们的互斥是为了保护共享数据的安全,因此之允许一个执行流访问临界资源,但是在判断临界资源时,由于没有人通知,我们只能频繁的一次又一次的去判断是否到达了访问条件,并且其他人也无法进入访问,这显然是浪费了资源,有了同步后,我们就可以回到家等通知,其他满足条件的线程则也可以进入,这不就增加了效率吗?所以说同步对互斥的缺点进行了补充。
生产消费者模型中,谁把数据放到队列里,谁把数据拿到,不是主要矛盾,处理数据需要多长时间,获取数据需要多长时间,这才是主要矛盾,生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

阻塞队列最经典的应用场景:管道

生产者消费者模型的优势:

解耦
支持并发
提高效率
平衡速度差异。

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

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

相关文章

C++ Qt 学习(三):无边框窗口设计

1. 无边框窗口 1.1 主窗口实现 MainWidget.h #pragma once#include <QtWidgets/QWidget> #include "CTitleBar.h" #include "CFrameLessWidgetBase.h"// 主窗口 MainWidget 继承自无边框窗口公用类 CFrameLessWidgetBase class MainWidget : publi…

FreeRTOS学习笔记(二)

一、时间片调度 1、同等优先级任务轮流地享有相同的 CPU 时间(可设置)&#xff0c; 叫时间片&#xff0c;在FreeRTOS中&#xff0c;一个时间片就等于SysTick 中断周期 /* 任务一&#xff0c;实现LED0每500ms翻转一次 */ void task1( void * pvParameters ) {uint32_t task1_n…

统计一个只包含大写字母的字符串中顺序对的数量.其中顺序对的定义为前面的字符小后面的字符大.例如在“ABC“中的顺序对为3,因为有AB,AC,BC

哈希法&#xff1a;扫描字符串&#xff0c;将出现的字符次数加1&#xff0c;统计比当前字符字典序小的字母出现的次数&#xff0c;即为顺序串的个数。 int CounSq(const char* arr)//时间复杂度O&#xff08;n&#xff09; {int sig[26] { 0 };int index 0;int sum 0;for (…

【自然语言处理】基于python的问答系统实现

一&#xff0c;文件准备 该问答系统是基于已知的问题和其一一对应的答案进行实现的。首先需要准备两个文本文件&#xff0c;分别命名为“question.txt”和“answer.txt”&#xff0c;分别是问题文件和答案文件&#xff0c;每一行是一个问题以及对应的答案。 问题文件: 中国的首…

C++ Qt 学习(四):自定义控件与 qss 应用

1. qss 简介 Qt style sheet&#xff08;qss&#xff0c;Qt 样式表&#xff09;&#xff0c;不需要用 C 代码控件进行重载&#xff0c;就可以修改控件外观&#xff0c;类似于前端的 css 2. qss 选择器 2.1 通配符选择器 /* 设置后控件窗口背景色都被修改为黄色 */ * {backg…

【OpenCV实现图像:用OpenCV图像处理技巧之白平衡算法】

文章目录 概要加载样例图像统计数据分析White Patch Algorithm小结 概要 白平衡技术在摄影和图像处理中扮演着至关重要的角色。在不同的光照条件下&#xff0c;相机可能无法准确地捕捉到物体的真实颜色&#xff0c;导致图像呈现出暗淡、色调不自然或者褪色的效果。为了解决这个…

项目实战:中央控制器实现(2)-优化Controller,将共性动作抽取到中央控制器

1、FruitController FruitController已经和Web没有关系了&#xff0c;和Web容器解耦&#xff0c;可以脱离Web容器做单元测试 package com.csdn.fruit.controller; import com.csdn.fruit.dto.PageInfo; import com.csdn.fruit.dto.PageQueryParam; import com.csdn.fruit.dto.R…

5G与物联网应用:新一代网络技术融合开创新时代

5G与物联网应用&#xff1a;新一代网络技术融合开创新时代 随着信息技术的不断演进&#xff0c;5G和物联网作为新一代网络技术&#xff0c;正在引领我们走向一个更加智能化、互联互通的新时代。本文将分析5G与物联网应用的技术原理、应用场景与发展趋势&#xff0c;并探讨它们…

软件测试|selenium执行js脚本

JavaScript是运行在客户端&#xff08;浏览器&#xff09;和服务器端的脚本语言&#xff0c;允许将静态网页转换为交互式网页。可以通过 Python Selenium WebDriver 执行 JavaScript 语句&#xff0c;在Web页面中进行js交互。那么js能做的事&#xff0c;Selenium应该大部分也能…

软件测试|测试方法论—边界值

边界值分析法是一种很实用的黑盒测试用例方法&#xff0c;它具有很强的发现故障的能力。边界值分析法也是作为对等价类划分法的补充&#xff0c;测试用例来自等价类的边界。 这个方法其实是在测试实践当中发现&#xff0c;Bug 往往出现在定义域或值域的边界上&#xff0c;而不…

Godot4实现游戏的多语言版本

要在Godot 4中实现多语言版本的游戏&#xff0c;您需要按照以下几个步骤来设置和管理游戏文本以及可能的其他资源&#xff0c;如图像或声音。以下是根据官方文档和详细教程整理的简明指南&#xff1a; 准备翻译文件&#xff1a; Godot支持使用.csv文件或.po文件进行国际化​​…

基于单片机GP2D12测距-proteus仿真-源程序

基于51单片机红外测距-proteus仿真-源程序 一、系统方案 本设计采用51单片机作为主控器&#xff0c;液晶1602显示&#xff0c;GP2D12采集距离值&#xff0c;按键设置报警阀值&#xff0c;测量值超过阀值&#xff0c;蜂鸣器报警。 二、硬件设计 原理图如下&#xff1a; 三、单…

Day45 力扣动态规划 : 1143.最长公共子序列 |1035.不相交的线 | 53. 最大子序和

Day45 力扣动态规划 : 1143.最长公共子序列 &#xff5c;1035.不相交的线 &#xff5c; 53. 最大子序和 1143.最长公共子序列第一印象看完题解的思路实现中的困难感悟代码 1035.不相交的线第一印象感悟代码 53. 最大子序和第一印象dp递推公式初始化遍历顺序 实现中的困难感悟代…

数字政府!3DCAT实时云渲染助推上海湾区数字孪生平台

数字孪生&#xff0c;是一种利用物理模型、传感器数据、运行历史等信息&#xff0c;在虚拟空间中构建实体对象或系统的精确映射&#xff0c;从而实现对其全生命周期的仿真、优化和管理的技术。数字孪生可以应用于各个领域&#xff0c;如工业制造、智慧城市、医疗健康、教育培训…

原厂可调漏电继电器 LLJ-125F Φ45导轨安装可选面板安装

LLJ-F(S)系列漏电继电器 原厂漏电继电器 LLJ-125F Φ45 导轨安装 系列型号&#xff1a; LLJ-10F(S)漏电继电器LLJ-15F(S)漏电继电器LLJ-16F(S)漏电继电器 LLJ-25F(S)漏电继电器LLJ-30F(S)漏电继电器LLJ-32F(S)漏电继电器 LLJ-60F(S)漏电继电器LLJ-63F(S)漏电继电器LLJ-80F(S)…

渲染管线详解

光栅化的渲染管线一般分为三大阶段&#xff1a;应用程序阶段->几何阶段->光栅化阶段 也可以四大阶段&#xff1a; 应用程序阶段->几何阶段->光栅化阶段->逐片元操作阶段 更详细的流程如下&#xff1a; Vertex Specification&#xff08;顶点规范化&#xff09…

一文搞定多端开发,做全栈大牛 附三大企业实战项目

一个功能三套代码 一改需求就是加不完的班&#xff1f; 不存在的&#xff0c;告别改改改 拥抱多端开发 一套代码搞定多个平台 高效开发&#xff1a;一套代码&#xff0c;多端通用 根据统计数据&#xff0c;全球移动设备用户数已经超过了50亿。随着智能手机、平板电脑等移动…

JSON——数组语法

一段JSON可能是以 ”{“ 开头 也可能仅包含一段JSON数组 如下 [ { "name" : "hello,world"}, {"name" : "SB JSON”}&#xff0c; {“name” : "SB互联网房地产CNM“}&#xff0c; ] 瞧&#xff0c;蛋疼不...CJSON过来还是得搜下网…

【ES专题】Logstash与FileBeat详解以及ELK整合详解

目录 前言阅读对象阅读导航前置知识笔记正文一、ELK架构1.1 经典的ELK1.2 整合消息队列Nginx架构 二、LogStash介绍2.1 Logstash核心概念2.1.1 Pipeline2.1.2 Event2.1.3 Codec (Code / Decode)2.1.4 Queue 2.2 Logstash数据传输原理2.3 Logstash的安装&#xff08;以windows为…

Python tkinter用iconphoto方法修改窗口标题的图片

修改Python Tkinter窗口的标题图片&#xff0c;可以使用PhotoImage、iconphoto方法。这个方法允许你设置窗口的图标。 运行结果 代码示例如下&#xff1a; import tkinter as tkroot Tk()# 加载图片&#xff0c;记住一定是要PNG图片 icon tk.PhotoImage(filephoto\\图片.pn…