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

news2024/12/23 6:13:51

👍作者主页:进击的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.coloradmin.cn/o/1186090.html

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

相关文章

软件测试用例与分类

测试用例与分类 黑盒测试基于需求的设计方法等价类边界值判定表正交表场景设计法错误猜测法 FiddlerPostman测试用例测试分类按测试对象界面测试可靠性测试容错性测试文档测试兼容性测试易用性安装卸载测试安全测试性能测试内存泄漏测试 白盒测试灰盒测试开发阶段单元测试集成测…

uniapp小程序接入腾讯云【增强版人脸核身接入】

文档地址&#xff1a;https://cloud.tencent.com/document/product/1007/56812 企业申请注册这边就不介绍了&#xff0c;根据官方文档去申请注册。 申请成功后&#xff0c;下载【微信小程序sdk】 一、解压sdk&#xff0c;创建wxcomponents文件夹 sdk解压后发现是原生小程序代…

django+drf+vue 简单系统搭建 (2) - drf 应用

按照本系统设置目的&#xff0c;是为了建立一些工具用来处理简单的文件。 1. 准备djangorestframework 关于drf的说明请参见&#xff1a;Django REST Framework教程 | 大江狗的博客 本系列直接使用drf的序列化等其他功能。 安装 conda install djangorestframework conda i…

第三届 “鹏城杯”(初赛)

第三届 “鹏城杯”&#xff08;初赛&#xff09; WEB Web-web1 反序列化tostring打Hack类 Payload:O%3A1%3A%22H%22%3A1%3A%7Bs%3A8%3A%22username%22%3BO%3A6%3A%22Hacker%22%3A2%3A%7Bs%3A11%3A%22%00Hacker%00exp%22%3BN%3Bs%3A11%3A%22%00Hacker%00cmd%22%3BN%3B%7D%7D…

【Java基础】Java容器相关知识小结

Java容器相关知识 0. 前言1. Collection接口1.1. List接口1.1.1. ArrayList1.1.2. LinkedList1.1.3. Vector1.1.4. Stack 1.2. Set接口1.2.1. HashSet1.2.2. LinkedHashSet1.2.3. TreeSet 1.3. Queue接口1.3.1. PriorityQueue1.3.2. LinkedList 2. Map接口2.1. HashMap2.2. Tre…

Verilog刷题[hdlbits] :Always casez

题目&#xff1a;Always casez Build a priority encoder for 8-bit inputs. Given an 8-bit vector, the output should report the first (least significant) bit in the vector that is 1. Report zero if the input vector has no bits that are high. For example, the …

大数据Doris(十九):数据导入(Load)

文章目录 数据导入(Load) 一、Broker load 二、Stream load 三、Insert 四、Multi load

RabbitMQ集群

RabbitMQ概述 1.RabbiMQ简介 RabbiMQ是⽤Erang开发的&#xff0c;集群⾮常⽅便&#xff0c;因为Erlang天⽣就是⼀⻔分布式语⾔&#xff0c;但其本身并不⽀持负载均衡。支持高并发&#xff0c;支持可扩展。支持AJAX&#xff0c;持久化&#xff0c;用于在分布式系统中存储转发消…

用Python实现朴素贝叶斯垃圾邮箱分类

一、实验目的 通过本实验&#xff0c;旨在使用朴素贝叶斯算法实现垃圾邮箱分类&#xff0c;并能够理解并掌握以下内容&#xff1a; 了解朴素贝叶斯算法的基本原理和应用场景。 学习如何对文本数据进行预处理&#xff0c;包括去除标点符号、转换为小写字母、分词等操作。 理解特…

Selenium alert 弹窗处理!

页面弹窗有 3 种类型&#xff1a; alert&#xff08;警告信息&#xff09;confirm&#xff08;确认信息&#xff09;prompt&#xff08;提示输入&#xff09; 对于页面出现的 alert 弹窗&#xff0c;Selenium 提供如下方法&#xff1a; 序号方法/属性描述1accept()接受2dismis…

图形验证码登录

图形验证码登录 添加图片标签&#xff0c;进入页面访问/api/verifyCode 1.html <img onclick"javascript:getvCode()" id"verifyimg" style"margin-left: 20px;"/><script>getvCode();/*** 获取验证码* 将验证码写到index.html页…

Collection集合 迭代器遍历Iterator 和集合增强For

迭代器遍历Iterator 标准写法: 增强For for(类型 名称 : 集合 ) 举例: 不仅可以集合也可以数组 底层仍然是iterator

python自动化测试(十一):写入、读取、修改Excel表格的数据

目录 一、写入 1.1 安装 xlwt 1.2 增加sheet页 1.2.1 新建sheet页 1.2.2 sheet页写入数据 1.2.3 excel保存 1.2.4 完整代码 1.2.5 同一坐标&#xff0c;重复写入 二、读取 2.1 安装读取模块 2.2 读取sheet页 2.2.1 序号读取shee页 2.2.2 通过sheet页的名称读取she…

OpenHarmony 社区运营报告(2023 年 10 月)

● 截至 2023 年 10 月&#xff0c;OpenHarmony 社区共有 51 家共建单位&#xff0c;累计超过 6200 名贡献者产生 24.2 万多个 PR&#xff0c;2.3 万多个 Star&#xff0c;6.1 万多个 Fork&#xff0c;59 个 SIG。 ● OpenHarmony 4.0 版本如期而至&#xff0c;开发套件同步升级…

【React】04.MVC模式和MVVM模式

React是Web前端框架 1、目前市面上比较主流的前端框架 ReactAngular&#xff08;NG框架&#xff09;Vue 主流的思想&#xff1a; 不在直接去操作DOM&#xff0c;而是改为“数据驱动思想” 操作DOM思想&#xff1a; 操作DOM比较消耗性能[主要原因就是&#xff0c;可能会导…

Javascript知识点详解:对象、New命令、Object对象的相关方法

目录 对象 对象是什么 构造函数 new 命令 基本用法 new 命令的原理 new.target Object.create() 创建实例对象 Object 对象的相关方法 Object.getPrototypeOf() Object.setPrototypeOf() Object.create() Object.prototype.isPrototypeOf() Object.prototype.__p…

微信定时发圈,让你轻松管理朋友圈!

有时候我们可能因为工作、生活等原因&#xff0c;错过了最佳的发布朋友圈时间。这时&#xff0c;就可以利用朋友圈的定时发送功能&#xff0c;提前编辑好朋友圈内容&#xff0c;设置好发布时间&#xff0c;让你的好友们在正确的时间看到你的动态。 但是怎么做到朋友圈定时发送…

学C++跟着视频学还是跟着书学?

学C跟着视频学还是跟着书学&#xff1f; 感觉得看基础和目标 如果不是喜欢 C 或者以求职 / 完成 C 相关工作为目标的话&#xff0c;菜鸟教程其实都够了&#xff0c;基本语法掌握就差不多&#xff0c;然后多去写。 最近很多小伙伴找我&#xff0c;说想要一些C的资料&#xff0…

无人机-地面站

借鉴于&#xff1a;https://www.yii666.com/blog/343453.html

网络工程实验记录

网络工程 show ip route show running-config 第一周 相同设备使用交叉线&#xff0c;不同设备之间使用直通线 R1能ping通10.1.1.1 R2能ping通所有的 R3能ping通172.16.1.1 即路由器只能到达自身线连接出去的&#xff0c;另一端就连接不了了。 此时给R1分配静态路由 R…