ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

共享线程池加持:RK3588双路视觉yolov5s推理方案

共享线程池加持:RK3588双路视觉yolov5s推理方案 香橙派5上的RK3588双路视觉方案做到第二版了。上一期咱们把两路视频的采集和编码流程打通这一期进入双路视觉方案二的核心阶段——共享线程池。系列教程写到这里已经是第十五篇前面那些基础内容系统镜像、RKNN工具链、单路yolov5s部署都已经趟平了路子这期直接上硬货共享线程池怎么设计、推理任务怎么分配、双路性能到底能跑到什么水平。这篇文章适合已经在RK3588板子上跑通单路yolov5s推理、对RKNN工具链有基本了解的朋友。就算你对线程池的核心机制还没吃透也没关系我会从设计思路一路讲到可落地的代码再到性能实测数据手把手把阶段一的内容完整过一遍。读完你能拿到一个经得住拷问的双路采集共享线程池yolov5s推理框架同时搞清楚下一步阶段二还能往哪个方向发力。1. 双路视觉方案的整体设计与阶段规划1.1 双路视觉不是简单加一路摄像头大家做双路视觉的需求五花八门有人是机器人前后双目避障需要同时感知前后障碍物有人是安防场景一台设备盯两个方向还有人做流水线检测一台主机顶住上下料两个工位。目标不同架构上的取舍差别就很大。单路部署时我们通常把采集、预处理、推理、后处理全写在一个while循环里代码简单直接、穿插少性能也稳定几乎不用考虑并发问题。一旦变成两路最粗放的做法就是开两个线程每个线程从头到尾跑一路——这方案在帧率不敏感、性能要求低的场景确实能跑但资源利用率非常差。两路画面的负载天然不同一路密集出目标、另一路几乎空闲的时候各自为政的两个线程互相感受不到对方的存在合起来看就是CPU和NPU忙闲不均整体吞吐量上不去。我在上一版双路方案里就踩过这个坑。两个采集线程各带一个推理线程CPU八核看着占用率不低但NPU推理本质是串行队列两个线程同时调rknn_run时内核层就会排队再加上预处理和后处理都在抢CPU整体帧率时好时坏平均下来远低于预期。后来换思路把每路一套线程改成全局统一一套共享线程池资源调度才算理顺。这一期的核心变化就在这两路摄像头流仍然各自采集但预处理和推理这些重活全部作为任务提交到同一个线程池里由固定数量的工作线程统一调度执行。这样做的好处是资源可控。线程池大小定死了CPU不会乱飘任务队列统一持有哪路任务多、哪路任务少调度逻辑可以全局做调整不会再出现一路忙死一路闲死的局面。对RK3588这种算力有限的板子来说这种以任务为中心的设计比以通道为中心的设计合理得多。1.2 阶段划分每一版解决什么问题整个香橙派双路视觉方案我是按三个阶段推进的。阶段一就是这篇文章覆盖的内容把双路采集和共享线程池的骨架搭起来yolov5s推理跑通结果能验证。阶段二做性能调优包括两路FPS均衡、动态丢帧策略、按场景调整线程池大小。阶段三做更复杂的扩展双路目标跟踪、跨路联动或者把检测结果通过MQTT推给上位机。这种划分方法最大的价值是验收标准清晰。阶段一我给自己定的验收标准是这样几条两路画面能同时显示并画出检测框系统持续运行20分钟内存不持续增长CPU占用没有相对单路翻倍式暴涨每路帧率稳定在8 FPS以上。达到这些就算合格剩下的细节优化全部丢给阶段二不在阶段一里无限纠缠。很多人在做这种多路方案时容易犯一个毛病一上来就想着把动态线程池、自动丢帧、帧率均衡全部实现结果代码写了八百行跑起来却处处是bug连最基础的双路功能都验证不了。我建议你也按阶段拆每个阶段只解决一个核心问题这样出问题时定位容易得多。1.3 共享线程池在方案里的位置共享线程池在整体架构里扮演的是承上启下的角色。双路摄像头是数据生产者它们持续产出原始帧共享线程池是任务消费者负责把每一帧变成检测结果。拿饭店打个比方两个厨房窗口不停出菜共享线程池就是后厨团队团队人数是固定的订单排队进菜单厨师按顺序做菜。不同点在于下单的不是一个窗口而是两个窗口同时往菜单上贴单后厨得自己决定先做哪一单。如果某个窗口出菜特别快、单子特别多后厨还可以决定暂时不接它的新单先把旧单清一清。具体到代码结构主循环负责从两个VideoCapture里读帧读到一帧就封装成一个FrameTask结构体包括通道号、图像数据、时间戳再提交到线程池。线程池里的工作线程取出任务后依次执行预处理、RKNN推理、后处理把带检测框的结果写回任务结构。主线程定期检查任务状态把已完成的结果画到窗口上。这就是阶段一的数据流主干后面所有代码都是围绕这条线展开的。2. 硬件资源摸底与方案选型2.1 RK3588(S)的预算够不够双路yolov5s先说结论够用但要省着花。香橙派5用的RK3588S有4个A76大核、4个A55小核NPU算力6 TOPS。yolov5s在640x640输入、int8量化条件下RK3588上单次推理普遍落在30到50毫秒之间单路能做到20到30 FPS。双路同时做推理单颗NPU是串行执行的两路加起来的上限就是单颗NPU的吞吐量平均下来每路大约只有10到15 FPS。这个帧率对很多检测场景其实够用安防看个来人、机器人判断有没有障碍物10 FPS级别完全能接受。但如果你要盯快速移动的物体每路15 FPS会有点吃力那就需要阶段二的隔帧推理策略主动放弃部分帧率把NPU时间腾出来给更关键的帧。CPU方面也别轻敌。预处理这部分BGR转RGB、letterbox缩放、归一化在A76大核上做一帧640x640大概要几毫秒两路叠加起来也是不小的负担。真正吃CPU的大头是后处理yolov5s要解析三个尺度的输出、做NMS如果这部分用Python写会比较伤建议直接用C实现至少NMS这层不要拖后腿。2.2 摄像头选型USB和MIPI CSI各有什么坑摄像头选型是双路方案里第一个要拍板的事。香橙派5板上有一路MIPI CSI接口USB口则很充裕。MIPI CSI的优点是延迟低、带宽稳定、不占CPU解码资源但缺点是驱动适配麻烦摄像头Sensor型号和官方BSP里的设备树配置如果不匹配经常出现内核识别不出、预览花屏、图像颜色不对等现象。USB摄像头走UVC协议就简单得多插上就能用OpenCV的VideoCapture直接读缺点是会占一点USB带宽和CPU。阶段一为了快速跑通架构我建议两路都用USB摄像头。等共享线程池框架稳定了再考虑把其中一路换成MIPI来降低延迟或提升画质。切换时只需要改摄像头初始化代码线程池部分完全不用动架构解耦的好处就在这里。如果你非要一开始就上MIPI记得先去确认你的Sensor型号在rk3588 Linux内核里有驱动支持再动手接线这个坑别硬趟我在群里见太多人卡在MIPI屏幕和摄像头的适配上了。2.3 为什么不每路各建一个线程池有人会问两路各建一个同大小的线程池不也一样能跑吗表面上看确实可以但实际跑起来差异非常大。两个独立线程池意味着CPU资源被静态地分成了两半。哪怕有一路完全空闲它那半边线程池也帮不上忙而忙碌的那一路只能在自己的资源池里打转。双路负载天然是错峰的——行人走到画面右侧右侧摄像头的目标变多左侧几乎空场各建线程池时右侧那一路瞬间就成了瓶颈左侧闲着的线程只能干瞪眼。共享线程池的逻辑是按任务分配、不按通道分配。所有任务进入同一个队列线程池谁空闲谁取忙碌一路的任务多、自然被处理得多资源自然流向了压力大的方向。配合任务优先级还能进一步做通道加权调度灵活性比固定线程池高一个量级。这一条是整个双路方案二的逻辑基石也是阶段一最值得理解透的点。3. 共享线程池的设计与实现3.1 线程池四件套队列、互斥锁、条件变量、工作线程共享线程池本质上就是一个固定数量的工作线程集合加上一个共享任务队列再用同步原语把两者串起来。任务队列用std::queue保存待执行函数对象工作线程从队列头部取任务执行互斥锁保证队列并发访问安全条件变量负责生产者和消费者之间的唤醒——任务入队时通知等待中的线程去取队列空时线程进入休眠等待。初学者最容易犯的错是图省事每帧直接new thread或者用std::async任务结束线程就销毁。这在轻负载下看不出问题但两路30 FPS意味着每秒要创建60个短命线程线程创建销毁和上下文切换的额外开销很快把CPU吃掉一截。线程池的意义就在于把线程创建好放在那里任务来了随取随用避免重复劳损。下面是阶段一里实际使用的SharedThreadPool代码参照常见实现做了轻量修改加了活跃任务计数和等待所有任务完成的方法方便在主循环里做结果汇总class SharedThreadPool { public: explicit SharedThreadPool(size_t threadCount) : stop_(false), activeTasks_(0) { for (size_t i 0; i threadCount; i) { workers_.emplace_back([this] { while (true) { std::functionvoid() task; { std::unique_lockstd::mutex lock(mutex_); condition_.wait(lock, [this] { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; task std::move(tasks_.front()); tasks_.pop(); activeTasks_; } task(); { std::lock_guardstd::mutex lock(mutex_); --activeTasks_; } doneCondition_.notify_all(); } }); } } templatetypename F void enqueue(F task) { { std::lock_guardstd::mutex lock(mutex_); tasks_.emplace(std::forwardF(task)); } condition_.notify_one(); } void waitAll() { std::unique_lockstd::mutex lock(mutex_); doneCondition_.wait(lock, [this] { return tasks_.empty() activeTasks_ 0; }); } ~SharedThreadPool() { { std::lock_guardstd::mutex lock(mutex_); stop_ true; } condition_.notify_all(); for (auto worker : workers_) { worker.join(); } } private: std::vectorstd::thread workers_; std::queuestd::functionvoid() tasks_; std::mutex mutex_; std::condition_variable condition_; std::condition_variable doneCondition_; bool stop_; int activeTasks_; };这段代码你直接抄走能用但有两个隐藏问题要留心下面单独说。3.2 共享线程池的坑任务生命周期和异常扩散直接跑上面这个线程池还要处理两个容易爆雷的细节。第一个是任务生命周期。如果你的任务lambda里捕获了外部对象的引用而那个对象在工作线程执行前就被销毁了程序直接给你演示段错误崩溃。所以任务里要么按值捕获把图像数据、结构体整体拷贝一份进来要么确保被引用对象的生命周期长于线程池。我建议阶段一里让FrameTask整体按值进队图像用cv::Mat浅拷贝数据引用计数会保住底层缓冲内存开销也可以接受。第二个是异常扩散。标准库的std::function不会捕获异常任务函数内部如果抛了异常工作线程直接终止池里的线程就少了一个。线程池不会自动补线程跑着跑着你发现线程全退光了程序卡死还找不出原因。所以任务函数内部必须自己try/catch或者在线程池的task()外层包一层try/catch记录日志。我在RKNN推理时遇到过模型偶尔抛错的情况如果不做这层保护程序可能跑半天就莫名其妙性能下降实际是线程悄悄退场了。3.3 任务里放什么推理任务的结构设计共享线程池里流转的是推理任务不是裸图像。我为每个任务定义了一个FrameTask结构struct DetResult { int classId; float confidence; cv::Rect box; // 原图像素坐标 }; struct FrameTask { int channel; // 0或1标记来源摄像头 cv::Mat frame; // 原始BGR帧 int64_t timestamp; // 采集时间戳单位毫秒 std::vectorDetResult results; // 检测结果存放处 bool finished false; // 是否已推理完成 };这个结构的好处是主线程提交任务后不需要同步等待可以继续去抓下一帧工作线程推理完成后把results填充回去主线程在合适时机检查finished标志并取出结果。这里有个细节需要注意finished标志会被工作线程写、主线程读属于跨线程访问。阶段一里因为同一任务只会被一个工作线程处理主线程只在抓帧间隙读取实际竞争很小但稳妥起见还是建议用std::atomic 包一层逻辑上更干净。把channel放在任务里非常关键。后处理时需要根据通道号做不同的显示叠加阶段二做优先级调度时也可以给channel 0更高的权重让它被优先处理。这些灵活性都来自共享线程池任务级调度的设计每路固定线程做不到这一点。4. 阶段一实操双路采集与预处理4.1 两路摄像头的初始化与参数配置阶段一实操从摄像头初始化开始。我这边的两路都用USB摄像头OpenCV的VideoCapture直接读设备节点cv::VideoCapture cap0(0, cv::CAP_V4L2); cv::VideoCapture cap1(1, cv::CAP_V4L2); if (!cap0.isOpened() || !cap1.isOpened()) { std::cerr Failed to open cameras std::endl; return -1; } cap0.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap0.set(cv::CAP_PROP_FRAME_HEIGHT, 640); cap0.set(cv::CAP_PROP_FPS, 30); cap1.set(cv::CAP_PROP_FRAME_WIDTH, 640); cap1.set(cv::CAP_PROP_FRAME_HEIGHT, 640); cap1.set(cv::CAP_PROP_FPS, 30);这里有个很实在的坑V4L2的设备编号不是百分百固定的。系统重启、USB口插拔顺序变化、驱动加载顺序变化都可能让0号和1号在开机的瞬间对调。务实做法是初始化时打印摄像头信息和分辨率做人工确认更严谨的做法是写udev规则按设备路径固定映射但阶段一没必要搞这么复杂打印确认就够了。分辨率我统一设成640x640一是因为yolov5s训练时输入就是这个尺寸二是两路640x640的数据量不算大内存和带宽压力都可控。如果你的摄像头支持1920x1080实际处理时还是要先缩到640x640不要直接把大图喂给模型前处理做resize更可控也能顺便降低带宽压力。4.2 两路画面不同步怎么办时间戳丢帧策略摄像头初始化后进入主循环两路抓帧。实际跑起来常遇到的情况是一路稳定30 FPS另一路实际输出只有25 FPS或者两路都存在帧间隔抖动。如果不做帧同步控制一路因为处理不过来把队列灌满另一路还在排队等待检测延迟会越来越大画面越来越飘。阶段一的解决方案是时间戳队列上限丢弃。每抓一帧记录采集时间戳每个通道最多允许有未完成的任务数N我取2。如果某通道已有的未完成任务达到上限直接把这一帧丢掉让该通道的队列始终只保留最新一帧或两帧。这样做本质上是用丢旧帧换实时性。检测会跳过一部分画面但延迟可控目标不会拖着很长的残影。推理永远处理的是相对较新的画面适合实时性要求高的场景。实现就在主循环提交任务前判断if (pendingCount[0] 2) { FrameTask t0{0, frame0, nowMs(), {}, false}; pool.enqueue([t0]() mutable { inferAndDraw(t0); }); pendingCount[0]; } if (pendingCount[1] 2) { FrameTask t1{1, frame1, nowMs(), {}, false}; pool.enqueue([t1]() mutable { inferAndDraw(t1); }); pendingCount[1]; }有同学问为什么不等两路在相同时刻抓帧再一起送模型答案是阶段一不需要。两路画面内容独立不算严格同步也不会影响检测效果。真正需要帧级同步的是双目测距或跨路融合推理那些放到后续阶段专门处理。4.3 预处理细节letterbox、RGB转换、归一化预处理这块坑最多我分开细说。yolov5s训练时的预处理包括letterbox等比例缩放加灰边、BGR转RGB、归一化到0到1区间、再转成NCHW布局。letterbox的核心是保持原始宽高比缩放多余部分填灰边灰边像素值通常用114。不要直接用resize拉伸拉伸会让画面里的物体比例变形检测精度显著下降。代码如下cv::Mat letterbox(const cv::Mat src, int targetSize 640) { float scale std::min( targetSize * 1.0f / src.cols, targetSize * 1.0f / src.rows); int newW round(src.cols * scale); int newH round(src.rows * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH)); cv::Mat canvas cv::Mat::zeros(targetSize, targetSize, CV_8UC3); canvas.setTo(cv::Scalar(114, 114, 114)); int dx (targetSize - newW) / 2; int dy (targetSize - newH) / 2; resized.copyTo(canvas(cv::Rect(dx, dy, newW, newH))); return canvas; }注意letterbox的填充位置dx、dy和缩放比例scale后处理做坐标逆变换时还要用把检测框坐标还原到原图分辨率否则画出来的框全歪。归一化这块有个容易多此一举的地方。如果你用的是int8量化的RKNN模型输入数据类型是uint8RKNN运行时内部会把像素值自动除以255你只需要做BGR转RGB和HWC到NCHW的布局切换不需要手动归一化。有些教程教人把输入转成float数组再手动归一化对量化模型纯属多余还白白增加计算量。真正需要手动归一化的是fp16模型但yolov5s在RK3588上基本都是int8部署。4.4 数据流闭环主循环怎么和线程池协作最后把主循环的数据流完整串一遍。主线程只做三件事抓帧、提交任务、检查结果。推理工作全在共享线程池里完成主循环不会被rknn_run卡住。流程是这样每抓到一对新帧先检查上一轮任务结果把检测完成的帧画上框并显示然后把新帧构造成FrameTask提交到线程池。提交后主线程不做任何等待立刻去抓下一帧。工作线程在后台执行预处理、推理、后处理完成时把results写回任务结构。由于队列设了上限即使工作线程速度跟不上抓帧速度旧帧会被丢弃不会无限积压。测试阶段建议用两张并排的namedWindow分别显示两路结果方便观察和调参。显示本身会占一点主线程时间但640x640图像的imshow开销很小不影响整体流程。阶段二如果要做更高帧率可以把显示放到独立线程或者直接用ffmpeg编码输出RTSP流。5. 双路yolov5s推理流程与性能测试5.1 RKNN模型准备从pt到rknn的流程回顾阶段一的推理依赖已转换好的RKNN模型。还没转过模型的同学我快速梳理一遍链路先在PC上用rknn-toolkit2把yolov5s的pt模型导出为onnx再用rknn-toolkit2加载onnx转成rknn格式。转换时可以做int8量化用训练集的抽样图片作为校准数据集生成量化表。转换完成后得到yolov5s.rknn拷贝到香橙派上备用。转换过程要特别留意模型输出的解析方式。yolov5s的原始输出是多尺度的转成rknn后输出维度、布局可能和PyTorch端有差异。最稳妥的方法是转换后先在PC上跑一遍模拟器把输出shape和数值打印出来确认和PyTorch输出一致再部署到板子上。我见过太多案例模型转出来了但输出解析写错检测框乱飞还以为是阈值没调好。在香橙派上加载模型的代码流程是rknn_init创建上下文、rknn_query查询输入输出属性、rknn_inputs_set设置输入、rknn_run执行推理、rknn_outputs_get获取输出。初始化只做一次之后循环推理不要每帧重新加载模型这是基本常识。5.2 为什么说RKNN调用不要多线程强上这里有一个整个方案里最重要的架构决策两路推理任务虽然都提交到共享线程池但线程池里真正执行rknn_run的逻辑必须串行化不能让多个工作线程同时调rknn_run。原因是RK3588的NPU执行推理时同一个模型上下文通常只允许一个推理线程占用。多个线程同时调用底层会互斥锁排队结果就是多线程的调度成本白白浪费性能不升反降。共享线程池解决的是CPU资源的统一分配但NPU本身是串行的你不可能通过加线程来让NPU变快。阶段一里我把线程池大小设为3但任务内部对RKNN推理加了互斥锁任何线程拿到任务后预处理和后处理可以并行做到rknn推理这一段必须排队等锁。实测这个方案比多线程硬上rknn_run稳定得多既保留了预处理并行的收益又避免了NPU并发调用的崩溃风险。这里贴一段推理锁的示意代码借助std::lock_guard保证同一时刻只有一个线程进入rknn_runstatic std::mutex rknnMutex; void inferWithLock(rknn_app_context_t ctx, FrameTask task) { // 预处理可以并行执行 cv::Mat resized letterbox(task.frame); // 到这里必须排队 std::lock_guardstd::mutex guard(rknnMutex); rknn_input inputs[1]; // ... 设置输入缓冲、调用rknn_run、取输出 }5.3 性能实测双路FPS、CPU占用、NPU占用阶段一我用上面的框架做了实测。环境是香橙派5两路USB摄像头分辨率640x640yolov5s int8量化RKNN模型共享线程池大小3。测出来的数据整理成下表指标单路参考值双路共享线程池阶段一每路推理FPS24 FPS约9到12 FPS双路总吞吐-约18到24 FPSCPU占用约25%约55%到65%内存占用约200 MB约380到420 MB稳定端到端延迟单帧约70 ms约120到150 ms这些数字说下直观感受每路9到12 FPS用来检测行人、车辆、日常物体都够用画面有轻微顿挫感但检测行为正常。CPU占用65%左右还有余量阶段二如果做动态线程池可以把线程个数调成CPU核心数的一半左右进一步压榨。内存稳定在400MB上下没有持续增长说明任务生命周期管理到位。端到端延迟120到150ms主要消耗在排队等待、预处理、NPU推理约40ms、后处理这几段。如果做贴近实时交互的场景像机器人避障这个延迟偏大阶段二至少要把排队延迟压掉一半。5.4 结果验证与常见失败表现性能数据测出来先别急着满足要验证检测结果对不对。双路画面分别显示检查三个点检测框是否贴合目标、目标类别是否正确、同一目标在连续帧中框的抖动是不是剧烈。框贴不贴合大概率是letterbox的逆变换参数写错了检查一下传给后处理的scale和偏移量。类别错乱去看模型量化校准用的dataset是不是和你的实际场景差异太大这种情况重做一个场景相关的校准集就能改善。如果出现检测框在画面边缘疯狂横跳多半是NMS阈值太低把低置信度框也放了出来建议先把conf阈值从0.25提到0.4看看效果。还有一种很常见的现象两路中某一路画面变黑或者花屏先直接检查摄像头硬件连接别在软件层排查半天。USB摄像头最常见的问题是供电不稳导致掉线给摄像头独立供电比什么软件优化都好使。6. 常见问题速查与个人实操笔记6.1 线程池任务堆积怎么排查阶段一运行过程中最典型的故障是任务堆积表现为主循环画面越来越卡、检测结果延迟持续增大、CPU占用居高不下。排查顺序建议这样走第一步看任务队列当前长度如果长时间处于上限说明消费速度跟不上生产速度。第二步看是不是某一路摄像头实际帧率远高于另一路疯狂往队列灌任务解决方法是给每个通道单独设置丢帧阈值不要共用同一个上限。第三步检查RKNN推理是否卡住在推理调用处加一个超时计时超过500ms就打日志定位NPU是否存在异常状态。实际跑下来我遇到最隐蔽的堆积原因是线程数设得太大。线程池用4个线程做预处理然后抢同一把推理锁大量CPU时间浪费在等待锁上面任务处理吞吐反而下降。之后把线程池降到3锁竞争压力骤减吞吐量反而上去了。这个反直觉的现象值得记一笔线程池不是线程越多越好线程越多锁竞争越厉害尤其在NPU串行调用这个前提下。6.2 画面撕裂和帧延迟如何处理画面撕裂主要发生在显示环节因为抓帧、推理、显示节奏不一致。最简单有效的处理方法是显示用双缓冲准备两个cv::Mat一个给工作线程写结果一个给主线程显示显示完毕后交换角色。OpenCV的imshow本身会把图像复制到内部缓冲直接复用同一变量连续写入会产生闪烁双缓冲能把这个现象压下去。帧延迟的根源多数在预处理耗时太长或者模型输入尺寸设置过大。如果你发现检测结果比画面慢半拍先用计时器把所有预处理、推理、后处理的耗时分别打出来找出耗时最大的环节再对症下药。实测下来A76大核上做640x640的BGR转RGB加letterbox单帧大概8到10ms。如果这个时间超过15ms去看看是不是代码里反复创建了中间的cv::Mat或者内存拷贝没走移动语义。6.3 内存持续升高怎么办内存持续增长是双路视觉方案最典型的稳定性问题原因百分之九十九都出在FrameTask里的cv::Mat没有明确的释放逻辑上。OpenCV的Mat用引用计数管理底层数据只要还有任何一个地方持有Mat引用底层图像缓冲就不会被释放积少成多就成了内存泄漏。排查方法是提交任务前记录待处理任务数在任务完成回调里确认Mat被清理主循环检查结果后要把任务里的frame显式设空断开引用链。另一个雷点在RKNN的输出缓冲rknn_outputs_get拿到输出后必须调用rknn_outputs_release释放不然后端缓存只增不减。我跑过一小时看到内存从400MB爬到1.2GB就是因为输出缓冲没释放。这两个坑叠在一起稳定性测试翻车两次很正常。6.4 共享线程池实现中容易踩的坑汇总最后把这一期实际踩过的、群里见别人踩过的零碎坑汇总成表写代码时对照着查问题症状原因对策任务队列为空时主线程卡死程序启动后不出画面条件变量等待逻辑写反检查wait条件要同时覆盖stop_和空队列线程池析构时崩溃程序退出段错误析构时还有任务在跑析构前先waitAll再置stop_多线程同时rknn_run偶发崩溃或性能骤降NPU上下文不支持并发推理段加互斥锁或用专用推理线程lambda捕获引用后对象销毁使用已销毁对象崩溃按引用捕获了栈对象改按值捕获或保证生命周期摄像头节点互换两路画面内容对调设备节点不稳定初始化时打印设备信息人工确认RKNN输出缓冲未释放内存持续增长忘记release每轮推理完成后必须release做完阶段一我自己最大的体会是共享线程池跑通之后后续想加第三路第四路视觉只需要新增一路摄像头初始化和一个通道号主体框架几乎不用改。线程池带来的架构红利就在这里资源调度和通道数量彻底解耦了。这也是一直强调在RK3588这种算力有限但接口丰富的板子上做视觉方案先把调度架构想清楚再叠功能的原因。等阶段一的代码跑顺以后你要做的无非是调优和扩展底子打好了后面走路都会稳很多。
返回列表