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

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

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

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

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

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

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

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

相关文章

MySQL的安装使用(入学篇)

目录 1 MySQL安装 1.1 安装epel源 1.2 安装MySQL Repository 1.3 安装MySQL官方yum源 1.4 安装服务端、客户端 1.5 启动MySQL服务 2 MySQL 使用 2.1 获取初始登录密码 2.2 登录MySQL数据库 2.3 修改密码 2.4 退出数据库 2.5 使用新密码登录数据库 2.6 重启数据库 2.7 创建数据…

【分享贴】需求变更、项目延误,项目经理应该如何应对?

案例分享&#xff1a; 项目经理小李跟进了一年半的项目&#xff0c;眼看着要到交付验收的阶段了&#xff0c;甲方的对接人却临时更换了&#xff0c;现在面临着一系列他无法处理的问题&#xff0c;项目目前推进困难。 案例背景&#xff1a; 小李在跟原甲方对接人合作时&#x…

Qt 事件循环

引出 UI程序之所叫UI程序&#xff0c;是因为需要与用户有交互&#xff0c;用户交互一般是通过鼠标键盘等的输入设备&#xff0c;那UI程序就需要有能随时响应用户交互的能力。 一个C程序的main函数大概是下面这样&#xff1a; int main() {...return 0; } 我们如何使程序能随…

ECharts中rich的使用

ECharts官方rich介绍 label: {// 在文本中&#xff0c;可以对部分文本采用 rich 中定义样式。// 这里需要在文本中使用标记符号&#xff1a;// {styleName|text content text content} 标记样式名。// 注意&#xff0c;换行仍是使用 \n。formatter: [{a|这段文本采用样式a},{b…

使用Nginx和Spring Gateway为SkyWalking的增加登录认证功能

文章目录 1、使用Nginx增加认证。2、使用Spring Gateway增加认证 SkyWalking的可视化后台是没有用户认证功能的&#xff0c;默认下所有知道地址的用户都能访问&#xff0c;官网是建议通过网关增加认证。 本文介绍通过Nginx和Spring Gateway两种方式 1、使用Nginx增加认证。 生…

大模型+人形机器人,用AI唤起钢筋铁骨

《经济参考报》11月8日刊发文章《多方布局人形机器人赛道,智能应用前景广》。文章称&#xff0c;工信部日前印发的《人形机器人创新发展指导意见》&#xff0c;按照谋划三年、展望五年的时间安排&#xff0c;对人形机器人创新发展作了战略部署。 从开发基于人工智能大模型的人…

原型制作神器ProtoPie的使用Unity与网页跨端交互

什么是ProtoPie&#xff1f; ProtoPie是一款面向设计师的软件原型设计工具&#xff0c;例如制作App界面交互展示&#xff0c;制作好的原型可以一键发布到Web服务器&#xff0c;就可以浏览器访问。由于其内置了大量常用交互类型&#xff0c;以及"程序化"模块&#xf…

【Mac开发环境搭建】Node.js安装(多版本切换)、Maven安装

文章目录 Node安装安装多个Node Maven安装下载配置环境变量修改配置文件settings.xml配置maven的本地仓库地址配置阿里云镜像仓库 IDEA使用 Node安装 https://nodejs.org/download/release/v16.20.1/ 如果对安装位置有要求&#xff0c;可以更改安装位置&#xff0c;不然直接点…

【开源分享】国内可用的免费安卓GPT语音助手 - 可音量键唤起,可联网

写在前面&#xff1a;这是一个我写的开源GPT语音助手&#xff0c;不收钱&#xff0c;只求Star! 简要介绍 这是一个基于ChatGPT的安卓端语音助手&#xff0c;允许用户通过手机音量键从任意界面唤起并直接进行语音交流&#xff0c;用最快捷的方式询问并获取回复 使用效果 一、基…

【干货】132道最新K8S面试题汇总~

k8s全称kubernetes&#xff0c;这个名字大家应该都不陌生&#xff0c;k8s是为容器服务而生的一个可移植容器的编排管理工具&#xff0c;越来越多的公司正在拥抱k8s&#xff0c;并且当前k8s已经主导了云业务流程&#xff0c;推动了微服务架构等热门技术的普及和落地&#xff0c;…

ChatGPT:如何安装使用插件?超详细的教程!

1.最简单的方法 直接使用油猴&#xff0c;里边能搜索到的插件都可以用 2.官方插件使用 ChatGPT Plus引入插件后&#xff0c;功能暴强许多&#xff0c;比如可以联网、可以生成图表、可以分析视频、可以与PDF交谈等。但有不少小伙伴还不知道怎么安装使用ChatGPT插件&#xff0c;所…

Python比较2个json数据是否相等

大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 如果有什么疑惑/资料需要的可以点击文章末尾名片领取源码 1、json数据转换成字典 dict1 json.load(load_f1) dict2 json.load(load_f2)2、将两个字典按key排好序&#xff0c;然后使用zip()函数将两个字典对应的key打包成元组。 …

MySQL 批量修改表的列名为小写

1、获取脚本 SELECT concat( alter table , TABLE_NAME, change column , COLUMN_NAME, , lower( COLUMN_NAME ), , COLUMN_TYPE, comment \, COLUMN_COMMENT, \; ) AS 脚本 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA 数据库名 and TABLE_NAME表名-- 大写是up…

win10 下 ros + Qt 工程CMakeLists.txt

win10 下 ros Qt 工程CMakeLists.txt 系统&#xff1a;win10 ros: melodic Qt: 5.12.12 源码目录: D:\workspace\catkin_qt 示例代码 https://github.com/ncnynl/ros-qt.git 由于示例代码是Qt4 &#xff0c;目前我是用QT5,所以CMakeLists.txt 修改如下 CMakeLists.txt #####…

数据跨领域应用实例—车辆通行大数据应用场景(二)

2023年10月25日&#xff0c;国家数据局正式揭牌。标志着我国数据基础制度正在不断完善&#xff0c;数据资源使用水平稳步提升&#xff0c;数据要素市场将进入发展快车道。当前&#xff0c;数字经济已成为我国经济高质量发展的新动能&#xff0c;国家数据局的成立&#xff0c;在…

互联网是如何运作的?以前端角度出发(b站objtube的卢克儿听课笔记)

1、你是如何用你的计算机设备发送数据和接受数据的呢&#xff1f; 例如我们是如何访问到哔哩哔哩的数据的 当你的设备开始连接到互联网时&#xff0c;会被分配一个IP地址。 而哔哩哔哩的服务器也接入到互联网&#xff0c;它也将被分配一个IP地址。 我们常说你访问某个网站&a…

测试老鸟整理,Postman接口测试MD5+RSA加密自动登录(超全总结)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 使用场景 大多数…

【MySQL】库操作和表操作

文章目录 一、库操作1. 创建数据库2. 数据库的编码问题查看系统默认支持的字符集查看数据库支持的字符集查看数据库支持的字符集校验规则校验规则对数据库的影响 3. 操纵数据库查看数据库显示创建语句修改数据库删除数据库 4. 数据库的备份和恢复数据库的备份数据库的恢复表的删…

外部访问K8S集群内部的kafka集群服务

不许转载 kafka 部署 把 kafka 部署到 k8s 后&#xff0c;我们肯定是通过 service 从 k8s 外部访问 kafaka。这里的 service 要么是 NodePort&#xff0c; 要么是 LoadBalancer 类型。我们使用的方式是 LoadBalancer。 我们先看下面这张图&#xff0c;这是 kafka 在集群中的网…

pytest一些常见的插件

Pytest拥有丰富的插件架构&#xff0c;超过800个以上的外部插件和活跃的社区&#xff0c;在PyPI项目中以“ pytest- *”为标识。 本篇将列举github标星超过两百的一些插件进行实战演示。 插件库地址&#xff1a;http://plugincompat.herokuapp.com/ 1、pytest-html&#xff…