熔池视频与红外MGS录像的异步存储
场景回顾与目标
- 场景:熔池视频(
MvPoolCamera)与红外 MGS(VideoPanelGL/MagDevice)录像的“启动/停止”在主线程执行导致 UI 卡顿和数据不刷新。 - 现状(已改进):将“启动录像”的协调逻辑放入线程池任务中执行,通过 
QMetaObject::invokeMethod(..., BlockingQueuedConnection)把真正的设备调用切回对象所属线程,熄弧时统一停止任务与设备,退出时限时收敛。 - 目标:系统性梳理 Qt 并发的三种主流形态(Worker-Object、QRunnable + QThreadPool、QtConcurrent),结合本需求给出基本用法、高阶使用与常见陷阱,帮助你在项目中“选对模型、用对场景、避开坑”。
 
Worker-Object 模式(QObject + QThread)
适用场景:
- 设备对象需要长期驻留在某个线程,有自己的事件循环(如定时器、异步 I/O)。
 - SDK 强制“同一线程内创建和调用”,或需要把采集、编码等持续性工作彻底从 GUI 脱离。
 - 需要可恢复、可监控的后台“服务型”对象。
 
核心思想:
- 使用 
QThread承载事件循环,把“设备 Worker 对象”通过moveToThread放到该线程。 - 所有对设备的调用通过信号/槽异步排队到 Worker 线程执行,返回通过信号回 GUI 线程。
 
示例(与本需求贴合:把红外与熔池设备迁移到专属线程):
// DeviceWorker.h
class DeviceWorker : public QObject {
    Q_OBJECT
public:
    explicit DeviceWorker(QObject* parent=nullptr) : QObject(parent) {}
    ~DeviceWorker() { /* 确保stop后再析构 */ }
public slots:
    void startRecording(const QString& filePath) {
        // 设备SDK要求:必须在对象所属线程执行
        // 初始化编码器/打开文件/启动保存...
        // 抛异常要转为错误信号,避免跨线程异常
        try {
            cameraStart(filePath); // 示例
            emit recordingStarted(filePath);
        } catch (const std::exception& e) {
            emit recordingFailed(QString::fromUtf8(e.what()));
        }
    }
    void stopRecording() {
        cameraStop(); // SDK 停止
        emit recordingStopped();
    }
signals:
    void recordingStarted(const QString& filePath);
    void recordingFailed(const QString& error);
    void recordingStopped();
private:
    void cameraStart(const QString& path);
    void cameraStop();
};
// 创建与退出(ProcessMonitor中)
QThread* irThread = new QThread(this);
auto* irWorker = new DeviceWorker();     // 创建于主线程 -> moveToThread
irWorker->moveToThread(irThread);
connect(irThread, &QThread::finished, irWorker, &QObject::deleteLater);
irThread->start();
// 调用启动(GUI线程发起,跨线程Queued)
QMetaObject::invokeMethod(irWorker, "startRecording",
                          Qt::QueuedConnection, Q_ARG(QString, irPath));
// 熄弧或析构时
QMetaObject::invokeMethod(irWorker, "stopRecording", Qt::QueuedConnection);
irThread->quit();
irThread->wait(3000); // 限时收敛
高阶实践:
- 若一个线程管理多个设备 Worker,按需引入任务路由与序列化(如 
QStateMachine、串行队列)。 - 如果设备需要自身的定时器或网络 I/O,必须在 Worker 所在线程创建(线程事件循环)。
 - 停止流程中要注意:先停任务,再停设备,再停线程;析构中用 
quit()+wait(),不要直接terminate()。 
常见陷阱:
- 在 GUI 线程创建 timer 并试图在 Worker 线程使用(无效)。定时器依赖所在线程事件循环。
 - 直接跨线程调用对象方法(非槽、非信号),会触发未定义行为。统一用信号/槽或 
invokeMethod。 - 错误的连接类型:跨线程默认是 Queued,但捕获 lambda 的上下文对象要存活且在正确线程。
 
适用本项目的演进方向:
- 将 
MvPoolCamera、VideoPanelGL或封装的“录像控制器”整体迁移至专属线程,录像启动/停止都排队到该线程。主线程只收 UI 事件与展示,彻底割裂设备与 UI 的耦合。 
QRunnable + QThreadPool(任务型并发)
适用场景:
- 一个或多个“启动/停止”的一次性工作单元需异步化(如录像启动,目录准备,数据库落盘)。
 - 不需要该任务长期常驻;任务完成即结束。
 - 需要统一管理并发、限制线程数量、避免频繁创建/销毁线程的开销。
 
本项目采用方案的核心点:
- “录像启动/停止”需在设备对象线程执行,但大量准备/协调逻辑(建目录、权限检查、路径生成等)可以放到任务线程。
 - 任务线程通过 
QMetaObject::invokeMethod(target, "slot", Qt::BlockingQueuedConnection, ...)把真正的设备操作切回目标对象的线程执行,且等待完成,以确保一致性。 - 熄弧或退出时,通过任务对象的原子取消标志收敛;并附加硬停止(直接调用设备 stop API)作为兜底。
 
基本用法(本项目同源):
class StartRecordTask : public QObject, public QRunnable {
    Q_OBJECT
public:
    StartRecordTask(QObject* deviceObj, const QString& path)
        : m_device(deviceObj), m_path(path), m_cancel(0) { setAutoDelete(false); }
    void run() override {
        if (m_cancel.loadAcquire()) return;
        // 复杂路径准备...
        // 切回设备对象线程 Blocking 调用,确保起停状态一致可判定
        bool ok = QMetaObject::invokeMethod(m_device, "startRecording",
                                            Qt::BlockingQueuedConnection, Q_ARG(QString, m_path));
        if (!ok) emit failed("invoke startRecording failed");
        else emit started(m_path);
    }
    void stop() { m_cancel.storeRelease(1); }
signals:
    void started(const QString& path);
    void failed(const QString& err);
private:
    QPointer<QObject> m_device;     // 生命周期安全
    QString m_path;
    QAtomicInt m_cancel;
};
// 提交到线程池
auto* task = new StartRecordTask(m_pVideoPanel, irPath);
connect(task, &StartRecordTask::started, this, &ProcessMonitor::onIRRecordingStarted);
connect(task, &StartRecordTask::failed, this, &ProcessMonitor::onIRRecordingFailed);
QThreadPool::globalInstance()->start(task);
高阶实践:
- 任务串行化:给同类任务加 
static QMutex避免重复启动;或在外层用“状态机”防抖。 - 线程池参数:
setMaxThreadCount(n);I/O 密集不等于多线程越多越好,控制到 2-4 足矣。 - 任务优先级:
threadPool->start(task, QThread::Priority)配合关键任务调度。 - 任务取消:设计“可取消点”(检查 
m_cancel),在耗时循环或阶段性子流程中尽早退出。 - 生命周期:使用 
QPointer与QObject父子关系,避免 UI 或设备对象销毁后任务仍访问。 
常见陷阱:
- 从池线程调用 GUI(或非线程安全)API;解决:严格 
invokeMethod切换到对象线程。 BlockingQueuedConnection引发死锁:如果错误地在同一线程里使用 Blocking,将阻塞自己。务必保证调用方与目标对象不在同一线程。- 忽略任务完成时的安全性:任务结束后信号回调中不要触碰已销毁对象,使用 
QPointer/QObject::destroyed监控。 
适用本项目:
- 我们已用该方案稳定解决“录像启动阻塞 UI”的问题;后续可把“停止录像”也封装为任务(现已在统一停止入口中做硬停止兜底)。
 
QtConcurrent(函数式任务与容器并行)
适用场景:
- 对“计算型/数据型”任务十分友好,例如批量文件/图像处理、数据map/reduce、并行转换。
 - 不适合直接操作有线程亲和性的 
QObject/设备对象。 
基本用法:
- 单次任务执行:
 
QFuture<void> f = QtConcurrent::run([=]{
    saveMapToCsv(map, path); // 纯函数/无QObject依赖
});
QFutureWatcher<void>* w = new QFutureWatcher<void>(this);
connect(w, &QFutureWatcher<void>::finished, this, []{ qDebug() << "csv saved"; });
w->setFuture(f);
- 容器并行:
 
auto results = QtConcurrent::mapped(files, [](const QString& f) {
    return computeHashOfFile(f);
});
高阶实践:
- 取消与进度:通过 
QFutureWatcher提供进度、取消事件,任务内部需主动检查QThread::currentThread()->isInterruptionRequested()或自定义标志实现“可取消点”。 - 合并结果:
mappedReduced做汇总,注意 reduce 函数的线程安全(或使用序列化 Reduce)。 - 混合调度:大量 CPU 任务使用 QtConcurrent;I/O 或设备相关用 Worker/QRunnable。
 
常见陷阱:
- 在 
QtConcurrent::run的函数中操作QObject/设备对象 → 违反线程亲和性,可能崩溃或未定义行为。 - 捕获 
this并在后台线程里直接操作 UI → 违规。 - 误以为 
cancel()能立即终止任务。QtConcurrent 的取消是“协作式”,任务需自查并尽早退出。 
适用本项目:
- 非设备相关、纯数据类任务(例如:CSV 写入、结果统计、图表数据重采样)可用 QtConcurrent,从 GUI 脱离。
 - 设备“起停录像/预览”等涉及线程亲和性,避免 QtConcurrent,改用 Worker/QRunnable + invokeMethod。
 
三者的选型建议与组合拳
- 设备起停、SDK 强线程约束:优先 Worker-Object(长期驻留线程),或 QRunnable 任务 + invokeMethod 切回对象线程。后者改动小、落地快;前者隔离更彻底。
 - 一次性协调任务、目录准备、数据库写入:QRunnable + 线程池。
 - CPU/数据密集型:QtConcurrent;避免牵涉 QObject/设备。
 - 统一的“停止收敛”策略:
- 停任务(置取消标志、互斥控制)→ 停设备(幂等 stop)→ 线程收敛(
waitForDone/quit+wait)→ 析构。 - 退出时设定全局 3-5s 的等待上限,日志标记未完成任务,避免卡死退出。
 
 - 停任务(置取消标志、互斥控制)→ 停设备(幂等 stop)→ 线程收敛(
 
结合现有实现的进一步优化点
- 将“停止录像”也封装任务(可选):对齐“启动”的一致性,让起停具备统一行为(同样通过 Blocking 切回对象线程停止),主线程只发命令。
 - 为录像任务添加“幂等防抖”:
- 若正在录像,重复启动直接忽略;
 - 若正在停止,重复停止直接忽略;
 - 状态表述细化:Idle / Starting / Recording / Stopping / Error。
 
 - 日志可观测性:
- 统一打印线程 ID、耗时(启动/停止时长)、返回码;
 - 启停异常自动降级策略(如重复尝试一次,或回退至安全态)。
 
 - I/O 专用线程池:
- 把CSV落盘等数据任务放入单独的池(max 1-2),避免与控制任务竞争线程资源。
 
 - 更清晰的对象边界:
- 抽象 
IRRecordingController、PoolRecordingController两个控制器,内部负责 Worker/任务细节,ProcessMonitor只负责 orchestrate。 
 - 抽象 
 
常见陷阱清单(易踩雷,务必规避)
- 直接跨线程调用 QObject 方法(非信号/槽/
invokeMethod)— 未定义行为。 - 在同一线程里对同对象使用 
BlockingQueuedConnection— 死锁。 - 线程未开启事件循环却使用了定时器/异步操作 — 无效。
 - 任务中捕获 UI 指针直接访问 — 线程安全问题。
 - 线程退出时未停止设备/未等待任务收敛 — 资源泄漏或崩溃。
 - 误解 QtConcurrent 的“取消”是强制杀死 — 不是,需要任务配合检查退出。
 
总结与落地结论
- 本项目采用的“QRunnable + QThreadPool + invokeMethod(Blocking)”方案,是在不大改结构的前提下,快速消除 UI 卡顿、保证线程安全和退出收敛的工程化解决方案。
 - 如需长期演进,建议将设备控制迁移到 Worker-Object 模式:设备专属线程、统一调度更清晰。
 - QtConcurrent 在本项目中适合 CSV 保存、数据处理、统计等与设备无关的任务。
 
                    
                
                
            
        
浙公网安备 33010602011771号