熔池视频与红外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 的上下文对象要存活且在正确线程。

适用本项目的演进方向:

  • MvPoolCameraVideoPanelGL 或封装的“录像控制器”整体迁移至专属线程,录像启动/停止都排队到该线程。主线程只收 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),在耗时循环或阶段性子流程中尽早退出。
  • 生命周期:使用 QPointerQObject 父子关系,避免 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 的等待上限,日志标记未完成任务,避免卡死退出。

结合现有实现的进一步优化点

  • 将“停止录像”也封装任务(可选):对齐“启动”的一致性,让起停具备统一行为(同样通过 Blocking 切回对象线程停止),主线程只发命令。
  • 为录像任务添加“幂等防抖”:
    • 若正在录像,重复启动直接忽略;
    • 若正在停止,重复停止直接忽略;
    • 状态表述细化:Idle / Starting / Recording / Stopping / Error。
  • 日志可观测性:
    • 统一打印线程 ID、耗时(启动/停止时长)、返回码;
    • 启停异常自动降级策略(如重复尝试一次,或回退至安全态)。
  • I/O 专用线程池:
    • 把CSV落盘等数据任务放入单独的池(max 1-2),避免与控制任务竞争线程资源。
  • 更清晰的对象边界:
    • 抽象 IRRecordingControllerPoolRecordingController 两个控制器,内部负责 Worker/任务细节,ProcessMonitor 只负责 orchestrate。

常见陷阱清单(易踩雷,务必规避)

  • 直接跨线程调用 QObject 方法(非信号/槽/invokeMethod)— 未定义行为。
  • 在同一线程里对同对象使用 BlockingQueuedConnection — 死锁。
  • 线程未开启事件循环却使用了定时器/异步操作 — 无效。
  • 任务中捕获 UI 指针直接访问 — 线程安全问题。
  • 线程退出时未停止设备/未等待任务收敛 — 资源泄漏或崩溃。
  • 误解 QtConcurrent 的“取消”是强制杀死 — 不是,需要任务配合检查退出。

总结与落地结论

  • 本项目采用的“QRunnable + QThreadPool + invokeMethod(Blocking)”方案,是在不大改结构的前提下,快速消除 UI 卡顿、保证线程安全和退出收敛的工程化解决方案。
  • 如需长期演进,建议将设备控制迁移到 Worker-Object 模式:设备专属线程、统一调度更清晰。
  • QtConcurrent 在本项目中适合 CSV 保存、数据处理、统计等与设备无关的任务。
posted @ 2025-08-21 14:27  非法关键字  阅读(18)  评论(0)    收藏  举报