Linux线程(四) 生产者消费者模型

目录

一、什么是生产者消费者模型

基本概念

优点以及应用场景

二、 基于阻塞队列的生产者消费者模型

三、POSIX信号量

四、基于环形队列的生产消费模型


一、什么是生产者消费者模型

        Linux下的生产者消费者模型是一种经典的多线程或多进程编程设计模式,它用于解决资源访问的同步问题,特别是在涉及任务分配、数据处理和资源共享的场景中。

基本概念

生产者:负责生成数据项并将其放入共享的缓冲区(队列)。当缓冲区满时,生产者可能需要等待(阻塞)直到有空间可用。

消费者:从缓冲区中取出数据项进行处理。如果缓冲区为空,消费者可能需要等待(阻塞)直到有新数据产生。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

优点以及应用场景

生产者消费者模型作为一种经典的并发设计模式,在软件开发中特别是涉及多线程或多进程协作的场景下,展现出诸多优势。

解耦:生产者和消费者之间通过共享缓冲区(如队列)进行间接通信,减少了直接的依赖关系,使得生产者和消费者的代码可以独立开发和维护,提高了模块的复用性和系统的灵活性。

支持并发:生产者和消费者通常作为独立的执行单元运行,可以并行工作,充分利用多核处理器的计算能力,提升系统整体的吞吐量和响应速度。

平衡资源利用:通过调整缓冲区的大小和管理生产者与消费者的数量,可以有效平衡生产速率和消费速率,防止生产过剩导致资源浪费或者消费过快导致资源饥饿,从而优化系统性能。

应用场景:

生产者消费者模型广泛应用于各种领域,如网络通信中的数据包处理、数据库的异步写入、GUI应用中的事件处理系统、多线程下载和处理等,任何需要解耦数据生产与数据消费过程的场景都可以考虑使用这一模式。 

二、 基于阻塞队列的生产者消费者模型

        在这个模型中,阻塞队列扮演了生产者和消费者之间的中介角色,它负责存储生产者产生的数据,并安全地传递给消费者处理。关键在于,阻塞队列能够自动管理同步问题,确保线程安全,同时提供阻塞机制来平衡生产与消费的速度。

阻塞队列属于仓库这一临界资源,而同一时刻只能有一个线程进入阻塞队列进行操作,所以要用到互斥锁,同时还要思考如果是消费者该如何知道有东西可以买了呢,如果是生产者如何知道仓库的东西不够了需要生产呢,这个时候就需要两个条件变量push_cond和pop_cond

关于条件变量在上篇文章中讲过,可以参考:

Linux线程(三)死锁与线程同步

push_cond

当生产者将阻塞队列放满时,就需要等待消费者消费完来唤醒生产者继续生产。

pop_cond

当消费者把队列消费空时,消费者会等待生产者往阻塞队列加资源后来唤醒消费者继续消费。 

接下来我们来实现一个基于阻塞队列的生产者消费者模型

访问阻塞队列一定会涉及到加锁,我们首先可以设计一个LockGuard(RAII)思想,利用类出作用域自动销毁来实现解锁,防止忘记解锁造成死锁。

LockGuard.hpp

#pragma once
#include <pthread.h>class Mutex
{
private:pthread_mutex_t* _mutex;public:Mutex(pthread_mutex_t* lock):_mutex(lock){}void Lock(){pthread_mutex_lock(_mutex);}void Unlock(){pthread_mutex_unlock(_mutex);}~Mutex(){}
};class LockGuard
{
private:Mutex _mutex;
public:LockGuard(pthread_mutex_t *lock):_mutex(lock){_mutex.Lock();}~LockGuard(){_mutex.Unlock();}
};

随后我们来实现一个任务类,模仿消费者拿到资源:

Task.hpp:

#pragma once
#include <iostream>
#include <string>const int defaultvalue = 0;enum
{ok = 0,div_zero,mod_zero,unknow
};const std::string opers = "+-*/%)(&";class Task
{
public:Task(){}Task(int x, int y, char op): data_x(x), data_y(y), oper(op), result(defaultvalue), code(ok){}void Run(){switch (oper){case '+':result = data_x + data_y;break;case '-':result = data_x - data_y;break;case '*':result = data_x * data_y;break;case '/':{if (data_y == 0)code = div_zero;elseresult = data_x / data_y;}break;case '%':{if (data_y == 0)code = mod_zero;elseresult = data_x % data_y;}break;default:code = unknow;break;}}void operator()(){Run();}std::string PrintTask(){std::string s;s = std::to_string(data_x);s += oper;s += std::to_string(data_y);s += "=?";return s;}std::string PrintResult(){std::string s;s = std::to_string(data_x);s += oper;s += std::to_string(data_y);s += "=";s += std::to_string(result);s += " [";s += std::to_string(code);s += "]";return s;}~Task(){}private:int data_x;int data_y;char oper; // + - * / %int result;int code; // 结果码,0: 结果可信 !0: 结果不可信,1,2,3,4
}; 

随后我们来实现一个阻塞队列,要注意一个时刻只能有一个线程访问,所以再push操作和pop操作时要加锁。

block_queue.hpp:

#pragma once
#include<iostream>
#include<queue>
#include<pthread.h>
#include"LockGuard.hpp"const int defaultcap=5;//默认容量为5
template<class T>
class block_queue
{
private:std::queue<T> _q;int _capacity;   //_q.size() == _capacity, 满了,不能在生产,_q.size() == 0, 空,不能消费了pthread_mutex_t _mutex;pthread_cond_t _push_cond;  //给生产者pthread_cond_t _pop_cond;  //给消费者// int _consumer_water_line;  // _consumer_water_line = _capacity / 3 * 2// int _productor_water_line; // _productor_water_line = _capacity / 3
public:block_queue(int cap=defaultcap):_capacity(cap){pthread_mutex_init(&_mutex,nullptr);pthread_cond_init(&_push_cond,nullptr);pthread_cond_init(&_pop_cond,nullptr);}bool IsFull(){return _q.size() == _capacity;}bool IsEmpty(){return _q.size() == 0;}void Push(const T &in){LockGuard lockguard(&_mutex);while(IsFull())pthread_cond_wait(&_push_cond,&_mutex);_q.push(in);//通知消费者可以消费了// if(_q.size() > _productor_water_line) pthread_cond_signal(&_c_cond);  // 也可以是当资源数量大于指定阈值时再通知pthread_cond_signal(&_pop_cond);}void Pop(T *out)//要取出任务{LockGuard lockgugrd(&_mutex);while(IsEmpty())pthread_cond_wait(&_pop_cond,&_mutex);*out=_q.front();_q.pop();//通知生产者可以生产了// if(_q.size() < _consumer_water_line) pthread_cond_signal(&_p_cond);      //也可以是当资源数量大于指定阈值时再通知pthread_cond_signal(&_push_cond);}~block_queue(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_push_cond);pthread_cond_destroy(&_pop_cond);}};

makefile文件,当然也可手动生成可执行文件,使用这个较为方便

test_block:Main.ccg++ -o  $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f test_block

使用Main.cc来测试这个模型:

#include"block_queue.hpp"
#include"Task.hpp"
#include<pthread.h>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>void *consumer(void *args)
{block_queue<Task> *bq=static_cast<block_queue<Task>* >(args);while(true){sleep(1);Task t;//取出任务bq->Pop(&t);t();//运行任务std::cout<<"consumer data: "<<t.PrintResult()<<std::endl;}return nullptr;
}void *producror(void *args)
{block_queue<Task> *bq=static_cast<block_queue<Task>*>(args);while(true){sleep(1);int x=rand()%10;usleep(rand()%123);int y=rand()%10;usleep(rand()%1234);char oper=opers[rand()%(opers.size())];Task t(x,y,oper);std::cout<<"productor data: "<<t.PrintTask()<<std::endl;bq->Push(t);}return nullptr;
}int main()
{srand((uint16_t)time(nullptr) ^ getpid() ^ pthread_self()); // 只是为了形成更随机的数据block_queue<Task> *bq=new block_queue<Task>();pthread_t c,p;pthread_create(&c,nullptr,consumer,bq);pthread_create(&p,nullptr,producror,bq);pthread_join(c,nullptr);pthread_join(p,nullptr);return 0;
}

运行结果如下

可以看到生产者每生产一个,消费者就拿到一个。

如果让生产者不休眠

可以看到消费者将阻塞队列填满,消费者取队首元素来执行。

总体流程就是:

生产过程:生产者创建数据项,并尝试将数据放入阻塞队列。如果队列已达到其容量限制,生产者的push()操作将被阻塞,直到队列中有空间可以添加新数据。

消费过程:消费者从阻塞队列中取出数据项进行处理。当队列为空时,消费者的pop()操作也会被阻塞,直到有新的数据被生产者放入队列。

通知与唤醒:一旦队列状态发生变化(例如有数据被放入或移出),阻塞队列会自动唤醒相应等待的线程,实现高效且线程安全的同步。 

三、POSIX信号量

POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。

在传统的基于阻塞队列的生产者消费者模型中,虽然使用阻塞队列自身可以避免一些复杂的同步问题,但确实存在这样一个情况:当生产者试图向满队列添加数据或消费者试图从空队列中取数据时,它们都需要对整个队列进行加锁,这实际上导致了生产者和消费者之间不必要的锁竞争。

比如生产者消费者模型,生产者只需要关注空间是否足够生产,消费者只需要关注资源是否足够消费,所以开始的时候生产者的信号量就是队列的大小,消费者的信号量就是0,当生成者生产一个资源,生产者信号量-1,消费者+1;当消费者消费一个资源, 生产者信号量+1,消费者-1。

使用POSIX信号量确实可以进一步优化这一模型,使得生产者与生产者之间、消费者与消费者之间存在锁竞争,而生产者和消费者之间不存在直接的锁竞争。这是因为信号量可以用来精确控制对资源的访问权限,而不仅仅是简单地锁定整个资源。

信号量的本质是一个计数器。

这个计数器用于跟踪某个资源(如共享内存区域、打印机等)的可用单位数。信号量机制通过这个计数器来控制多个进程或线程对共享资源的访问,确保资源的合理分配和同步。通过这个计数器的增加和减少,信号量不仅能够控制访问权限,还能协调进程间的同步,是解决并发控制问题的一种有效工具。  

 初始化信号量

#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表示线程间共享,非零表示进程间共享
value:信号量初始值
销毁信号量
int sem_destroy(sem_t *sem);
等待信号量(P操作)
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
发布信号量(V操作)
功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
上一个生产者 - 消费者的例子是基于阻塞队列 , 其空间可以动态分配 , 现在基于固定大小的环形队列重写这个程序

四、基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性
生产者只需要关注空间 spaceSem 是否足够生产,消费者只需要关注资源 dataSem 是否足够消费,所以开始的时候生产者的信号量就是队列的大小,消费者的信号量就是0,当生成者生产一个资源,生产者信号量-1,消费者+1;当消费者消费一个资源, 生产者信号量+1,消费者-1。

如图所示

代码示例,基于环形队列的生产消费模型其中的资源依旧使用Task来模拟

ringqueue.hpp

#pragma once#include <iostream>
#include <vector>
#include <semaphore.h>
#include "LockGuard.hpp"// 定义默认队列大小
const int defaultSize = 5;// 泛型环形队列类模板
template <typename T>
class RingQueue
{
private:// 信号量P操作,减少信号量计数,若计数<0则阻塞当前线程void P(sem_t &sem){sem_wait(&sem);}// 信号量V操作,增加信号量计数,若唤醒等待的线程void V(sem_t &sem){sem_post(&sem);}public:// 构造函数,初始化环形队列RingQueue(int size = defaultSize): _ringQueue(size), _size(size), _prodStep(0), _consStep(0){// 初始化空间信号量,初始值为队列大小,表示初始时所有空间都是空闲的sem_init(&_spaceSem, 0, size);// 初始化数据信号量,初始值为0,表示队列初始无数据sem_init(&_dataSem, 0, 0);// 初始化生产者和消费者的互斥锁,保护各自的操作步骤pthread_mutex_init(&_prodMutex, nullptr);pthread_mutex_init(&_consMutex, nullptr);}// 向队列添加元素void Push(const T &item){// 1. 减少空间信号量,尝试获取生产空间,若无空间则阻塞P(_spaceSem);{// 2. 加生产者锁,确保生产操作的原子性LockGuard lockGuard(&_prodMutex);// 执行实际的入队操作_ringQueue[_prodStep] = item;_prodStep++;        // 移动生产指针_prodStep %= _size; // 环状处理边界}V(_dataSem);}// 从队列移除元素void Pop(T *outItem){// 1. 减少数据信号量,尝试获取数据,若无数据则阻塞P(_dataSem);{// 2. 加消费者锁,确保消费操作的原子性LockGuard lockGuard(&_consMutex);// 执行实际的出队操作*outItem = _ringQueue[_consStep];_consStep++;        // 移动消费指针_consStep %= _size; // 环状处理边界}//消费者V操作时不冲突,可以解锁 信号量的P操作(wait/减)和V操作(signal/增)都是原子操作。V(_spaceSem);}// 析构函数,释放资源~RingQueue(){sem_destroy(&_spaceSem);sem_destroy(&_dataSem);pthread_mutex_destroy(&_prodMutex);pthread_mutex_destroy(&_consMutex);}private:// 环形队列底层使用std::vector存储std::vector<T> _ringQueue;int _size; // 队列大小// 生产者和消费者的步进索引int _prodStep;int _consStep;// 信号量,管理空间和数据的可用性sem_t _spaceSem;sem_t _dataSem;// 互斥锁,分别保护生产者和消费者的步骤更新pthread_mutex_t _prodMutex;pthread_mutex_t _consMutex;
};

Main.cc

#include"ringqueue.hpp"
#include "Task.hpp"
#include <unistd.h>
#include <pthread.h>
#include <ctime>void *Productor(void *args)
{// sleep(5);RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){// 数据怎么来的?// 1. 有数据,从具体场景中来,从网络中拿数据// 生产前,你的任务从哪里来的呢???int data1 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODOusleep(rand() % 123);int data2 = rand() % 10; // [1, 10] // 将来深刻理解生产消费,就要从这里入手,TODOusleep(rand() % 123);char oper = opers[rand() % (opers.size())];Task t(data1, data2, oper);std::cout << "productor task: " << t.PrintTask() << std::endl;// rq->push();rq->Push(t);sleep(1);}
}void *Consumer(void *args)
{RingQueue<Task> *rq = static_cast<RingQueue<Task> *>(args);while (true){// sleep(1);Task t;rq->Pop(&t);t();std::cout << "consumer done, data is : " << t.PrintResult() << std::endl;}
}int main()
{srand((uint64_t)time(nullptr) ^ pthread_self());pthread_t c[3], p[2];// 唤醒队列中只能放置整形???// RingQueue<int> *rq = new RingQueue<int>();RingQueue<Task> *rq = new RingQueue<Task>();pthread_create(&p[0], nullptr, Productor, rq);pthread_create(&p[1], nullptr, Productor, rq);pthread_create(&c[0], nullptr, Consumer, rq);pthread_create(&c[1], nullptr, Consumer, rq);pthread_create(&c[2], nullptr, Consumer, rq);pthread_join(p[0], nullptr);pthread_join(p[1], nullptr);pthread_join(c[0], nullptr);pthread_join(c[1], nullptr);pthread_join(c[2], nullptr);return 0;
}

运行如图所示 

环形队列中的生产者和消费者通过同步与互斥机制维持着一种动态平衡,确保数据的连续生产和消费,体现了典型的生产者-消费者问题的解决方案。 

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

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

相关文章

【LangChain系列 15】语言模型——LLMs(一)

原文地址&#xff1a;【LangChain系列 15】语言模型——LLMs(一) 本文速读&#xff1a; 异步API 自定义LLM Fake LLM HumanInput LLM 本文将介绍LLMs在LangChain中的一些用法&#xff0c;帮助我们更好地了解LLM模块。 01 异步API LangChain通过异步库实现了对异步的支持&a…

大模型应用的最佳实践Chains, SequentialChain使用示例

各种chain的介绍 串联式编排调用链:SequentialChain 流水线 胶水代码逻辑处理具备编排逻辑 串行 one by one的调用上一个chain的输出 作为 下一个chain的输入 超长文本的转换 Transform Chain pdf文件处理提供了套壳的能力 将python处理字符串的能力 套用进来 完成数据的格式化…

java sql中 大于 小于 大于等于 小于等于 代替符号

在写java时sql会经常会忘记大于小于号的表示方法导致无法运行&#xff0c;总结一下 第一种方法&#xff1a; < &#xff1a;< < &#xff1a; < &#xff1a;> &#xff1a; > sql如下&#xff1a; create_at > #{startTime} and create_at < #{end…

MySQL innodb_buffer_pool_size 相关常用语句

对于MySQL速度慢的问题&#xff0c;除了优化 SQL 以外&#xff0c;应该必须优先想到的即使 MySQL 数据库的 innodb_buffer_pool_size 配置问题。 一般来说&#xff0c;innodb_buffer_pool_size 的默认大小都是很小的&#xff0c;尤其是 win 下其默认大小更是只有离谱的 8M。Li…

centos7中如何全局搜索一下nginx的配置文件?

在CentOS 7中搜索Nginx的配置文件&#xff0c;你可以使用一些常用的命令行工具&#xff0c;比如find、grep等。这些工具可以帮助你在文件系统中查找文件&#xff0c;也可以用来查找Docker容器内部的文件&#xff0c;只要你知道如何访问容器的文件系统。 1. 搜索系统中的Nginx配…

深度学习面试题整理

文章目录 1. TensorFlow是什么&#xff1f;2. 计算图3. pytorch tensorflow4. 节点与张量类型5. tensorboard6. tensflow三个工作组件7. 大多数 TensorFlow 算法的常用步骤是什么&#xff1f;8. 处理TensorFlow中过拟合的方法9. 为什么出现过拟合10. 交叉验证11. 学习率12. 特征…

Python SMTP发送邮件时如何设置邮件地址?

Python SMTP发送邮件如何添加附件&#xff1f;如何使用SMTP发信&#xff1f; Python则通过其内置的smtplib模块和email模块为我们提供了实现这一功能的工具。在发送邮件的过程中&#xff0c;正确设置邮件地址是至关重要的&#xff0c;AokSend就来详细探讨一下如何在Python SMT…

Python专题:十三、日期和时间(1)

Python 日期和时间处理模块 模块就是别人写好的代码&#xff0c;通过将模块引入到代码里&#xff0c;使用已经实现好的功能 math模块&#xff0c;import模块名 time模块 时间戳&#xff1a;从公元1970年1月1日0点0分0秒起&#xff0c;到现在总共经历过的秒杀

odoo16 银行对账单导入改造

解决问题: odoo原生功能的话 是不能在系统上临时处理文件内容的&#xff0c;只会提示文件内容格式不对。 原始文件格式 在头部与尾部 格式问题&#xff0c;例如csv文件和 C53 文件&#xff0c;做一个前置弹框处理数据之后再导入 camt效果: csv效果:

汇聚荣电商:拼多多开店需要多少费用?

想要在拼多多这个巨大的电商平台上开一家属于自己的店铺&#xff0c;很多创业者都会关心一个问题&#xff1a;开店需要多少费用?答案并不复杂&#xff0c;但背后的经营哲学和策略却值得深究。接下来&#xff0c;让我们从四个不同的方面来详细探讨这个问题。 一、开店成本分析 …

GPT-4o,AI实时视频通话丝滑如人类,Plus功能免费可用

不开玩笑&#xff0c;电影《她》真的来了。 OpenAI最新旗舰大模型GPT-4o&#xff0c;不仅免费可用&#xff0c;能力更是横跨听、看、说&#xff0c;丝滑流畅毫无延迟&#xff0c;就像在打一个视频电话。 现场直播的效果更是炸裂&#xff1a; 它能感受到你的呼吸节奏&#xf…

10G UDP协议栈 IP层设计-(6)IP TX模块

一、模块功能 1、上层数据封装IP报文头部 2、计算首部校验和 二、首部校验和计算方法 在发送方&#xff0c;先把IP数据报首部划分为许多16位字的序列&#xff0c;并把检验和字段置零。用反码算术运算把所有16位字相加后&#xff0c;将得到的和的反码写入检验和字段。接收方收…

C++(week2):C语言中高级

文章目录 (八) 指针0.概念1.指针基础(1)指针的声明(2)指针的两个基本操作①取地址运算符 &②解引用运算符 * (3)野指针①野指针②空指针③指针变量的赋值 vs 指针变量指向对象的赋值 (4)指针的应用①指针作为参数进行传递②指针作为返回值③拓展&#xff1a;栈帧 (5)常量指…

手撸XXL-JOB(一)——定时任务的执行

SpringBoot执行定时任务 对于定时任务的执行&#xff0c;SpringBoot提供了三种创建方式&#xff1a; 1&#xff09;基于注解(Scheduled) 2&#xff09;基于接口&#xff08;SchedulingConfigurer&#xff09; 3&#xff09;基于注解设定多线程定时任务 基于Scheduled注解 首…

基于51单片机的冰箱控制系统设计( proteus仿真+程序+设计报告+原理图+讲解视频)

基于51单片机冰箱控制系统设计( proteus仿真程序设计报告原理图讲解视频&#xff09; 基于51单片机冰箱控制系统设计 1. 主要功能&#xff1a;2. 讲解视频&#xff1a;3. 仿真4. 程序代码5. 设计报告6. 原理图7. 设计资料内容清单&&下载链接资料下载链接&#xff1a; …

【C++】学习笔记——继承_2

文章目录 十二、继承5. 继承与友元6. 继承与静态成员7. 复杂的菱形继承及菱形虚拟继承 未完待续 十二、继承 5. 继承与友元 友元关系不能继承&#xff0c;也就是说父类友元不能访问子类私有和保护成员 。除非子类也设置成友元。 6. 继承与静态成员 父类定义了 static 静态成…

pnpm:无法加载文件 C:\Users\PC\AppData\Roaming\npm\pnpm.ps1,因为在此系统上禁止运行脚本。

使用pnpm命令启动vue时报了个错&#xff1a; 解决起来也简单&#xff0c;右击开始菜单&#xff0c;用管理员身份打开终端。win11的如下图&#xff1a; win10我记得应该是PowerShell&#xff08;管理员&#xff09;&#xff0c;这样的。 打开之后执行命令&#xff1a; set-…

物联网平台之单体架构

介绍本文主要介绍平台的单体架构&#xff0c;包括各个组件之间的数据流描述以及所做的一些架构选择。在单体架构模式下&#xff0c;所有 ThingsKit 组件都在单个 Java 虚拟机 (JVM) 中启动&#xff0c;并共享相同的操作系统资源。由于 ThingsKit 是用 Java 编写的&#xff0c;因…

dnf手游攻略,新手入坑必备!

一、角色创建策略 在DNF手游中&#xff0c;角色创建是玩家初入游戏的首要步骤。为最大化游戏体验和收益&#xff0c;新手玩家通常建议创建三个角色&#xff1a;一个主账号和两个副账号。 主账号选择 主账号的选择应基于玩家个人的喜好和对职业的熟悉程度。无论选择哪个职业&a…

番外篇 | 手把手教你利用YOLOv8进行热力图可视化 | 针对视频

前言:Hello大家好,我是小哥谈。YOLOv8的热力图可视化可以帮助我们更加直观地了解模型在图像中的检测情况,同时也可以帮助我们进行模型的调试和优化。热力图是一种颜色渐变的图像,不同颜色的区域表示不同程度的关注度或者置信度。在YOLOv8中,可以通过设置阈值来控制热力图的…