Qt多线程图像显示同步与程序退出卡死问题咨询
问题背景
我们基于Qt开发了一款图像处理与显示程序,采用子线程执行图像处理任务,主线程负责UI显示。为实现显示同步,我们在连接信号槽时使用Qt::BlockingQueuedConnection参数;由于其他线程可能覆盖图像数据,我们引入了锁机制做保护,代码如下:
ProcessThread 实现
class ProcessThread : public QThread { Q_OBJECT public: ProcessThread(QObject *parent = nullptr); void run() override { int i=0; while (1) { if(isQuit)break; mutex.lock(); QString filename=QString("/home/%1.bmp").arg((i++)%2000); data=imread(filename.toStdString()); //dosomthing... emit processCompleted(&data); mutex.unlock(); } } void setIsQuit(bool v){isQuit=v;} signals: void processCompleted(const Mat *data); private: bool isQuit=false; Mat data; QMutex mutex; };
Widget 实现
class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr): QWidget(parent) { thread=new ProcessThread(); connect(thread,SIGNAL(processCompleted(const Mat *)), this,SLOT(processCompleted(const Mat *)),Qt::BlockingQueuedConnection); thread->start(); } ~Widget() { thread->setIsQuit(true); thread->quit(); thread->wait(); delete thread; } void paintEvent(QPaintEvent *event) override { Q_UNUSED(event) QPainter p(this); p.drawImage(QPoint(0,0),image); } private slots: void processCompleted(const Mat *data) { if(data!=nullptr) { if(data->channels()==1) { image=QImage(data->data, data->cols, data->rows, data->step, QImage::Format_Grayscale8); } else { image=QImage(data->data, data->cols, data->rows, data->step, QImage::Format_RGB888); } } else { image=QImage(); } repaint(); } private: QImage image; ProcessThread *thread; };
main 函数
int main(int argc, char *argv[]) { QApplication a(argc, argv); Widget w; w.show(); return a.exec(); }
问题现象
- 退出程序时,执行流程会卡在
~Widget()析构函数中的thread->wait()调用处,无法继续向下执行; - 若注释掉
thread->wait()语句,程序退出时不会出现卡死,但会提示QMutex: destroying locked mutex和QThread: Destroyed while thread is still running两类错误; - 若信号槽连接时不使用
Qt::BlockingQueuedConnection参数,上述退出卡死问题不会复现,但无法实现预期的显示同步效果。
根因分析
卡死的核心是死锁,逻辑如下:
Qt::BlockingQueuedConnection的机制是:发送信号的线程会一直阻塞,直到槽函数在接收者所属线程执行完成才会返回。- 程序退出时,主线程执行析构函数,调用
thread->wait()后会一直阻塞,等待子线程执行完毕退出,此时主线程不会再处理任何事件循环中的任务。 - 如果子线程此时刚好执行到
emit processCompleted(&data),就会阻塞等待主线程执行对应的槽函数;但主线程卡在wait()根本不会处理事件、执行槽,子线程就永远无法继续运行、退出循环。 - 主线程等子线程退出,子线程等主线程执行槽,形成循环等待,就卡死了。
另外代码还有三个隐藏问题:
- 重写
QThread::run()后内部是while循环,没有调用exec()开启线程事件循环,thread->quit()调用完全无效。 - 跨线程传递Mat裸指针,且构造QImage时直接引用Mat的内存地址不做深拷贝,就算不卡死,子线程后续覆盖
data成员时,主线程的QImage会访问野指针,触发崩溃或显示花屏。 - 锁粒度过大:锁从读取图像开始持有,直到信号发送、槽执行完成才释放,持有锁期间跨线程阻塞,进一步放大死锁概率。
- 原代码没有做颜色空间转换:OpenCV默认读取的彩色图是BGR格式,直接构造RGB888格式的QImage会出现颜色显示错误。
修复方案
不需要依赖Qt::BlockingQueuedConnection也能实现可靠的帧同步,同时彻底避免死锁,修改点如下:
- 移除无效的quit()调用,修正退出流程:析构时先设置退出标志,先处理完主线程所有待处理事件,再等待子线程退出,避免子线程卡在信号发送阶段。
- 跨线程传值而非传裸指针:信号传递Mat对象(Mat本身带引用计数,传值开销极低),主线程收到后立刻做深拷贝转成QImage,不需要依赖锁保护共享的
data成员,彻底消除野指针风险。 - 缩小锁范围,移除不必要的锁:子线程把处理后的Mat作为局部变量通过信号传出,不需要锁保护共享变量;同步逻辑用QWaitCondition实现,锁仅覆盖等待条件判断的极短片段。
- 用显式帧应答实现同步:如果需要严格保证“处理完一帧、显示完一帧再处理下一帧”的同步逻辑,增加一个主线程到子线程的应答信号,主线程完成绘制后发信号通知子线程可以处理下一帧,比
BlockingQueuedConnection更可控,不会出现退出死锁。
修正后核心代码参考
ProcessThread 修改
class ProcessThread : public QThread { Q_OBJECT public: ProcessThread(QObject *parent = nullptr) {} void run() override { int i=0; while (!isQuit) { QString filename=QString("/home/%1.bmp").arg((i++)%2000); Mat frame = imread(filename.toStdString()); // 图像处理逻辑直接操作局部变量frame,不需要锁 // dosomething... // 传值发出信号,不需要传裸指针 emit frameReady(frame); // 阻塞等待主线程显示完成的应答,实现同步,加超时避免异常卡死 QMutexLocker locker(&mutex); waitCondition.wait(&mutex, 1000); if (isQuit) break; } } void setIsQuit(bool v){ QMutexLocker locker(&mutex); isQuit=v; waitCondition.wakeAll(); // 唤醒可能阻塞在等待条件的线程 } public slots: // 主线程显示完成后调用该槽,通知子线程处理下一帧 void onFrameDisplayed() { QMutexLocker locker(&mutex); waitCondition.wakeAll(); } signals: void frameReady(Mat frame); // 传值,不是指针 private: bool isQuit=false; QMutex mutex; QWaitCondition waitCondition; // 用等待条件做同步更可控 };
Widget 修改
class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr): QWidget(parent) { thread=new ProcessThread(); // 用默认的Qt::AutoConnection即可,跨线程自动走队列连接,不会阻塞 connect(thread, &ProcessThread::frameReady, this, &Widget::onFrameReady); // 连接显示完成的应答信号 connect(this, &Widget::frameDisplayed, thread, &ProcessThread::onFrameDisplayed); thread->start(); } ~Widget() { thread->setIsQuit(true); // 先处理完所有待处理的事件,保证队列里的信号都处理完 QCoreApplication::processEvents(); thread->wait(); // 此时子线程已经能正常退出,不会卡死 delete thread; } void paintEvent(QPaintEvent *event) override { Q_UNUSED(event) QPainter p(this); p.drawImage(QPoint(0,0),image); // 绘制完成后发应答信号,通知子线程可以处理下一帧 emit frameDisplayed(); } private slots: void onFrameReady(Mat frame) // 传值接收,直接拿到独立的帧数据 { if(frame.empty()) { image = QImage(); } else { if(frame.channels() == 1) { image = QImage(frame.data, frame.cols, frame.rows, frame.step, QImage::Format_Grayscale8).copy(); } else { Mat rgbFrame; cvtColor(frame, rgbFrame, COLOR_BGR2RGB); // OpenCV默认BGR,必须转成RGB才能正常显示 image = QImage(rgbFrame.data, rgbFrame.cols, rgbFrame.rows, rgbFrame.step, QImage::Format_RGB888).copy(); } // 调用copy()做深拷贝,避免后续frame释放后QImage访问野指针 } update(); // 用update()代替repaint(),Qt会自动合并绘制事件,性能更好 } signals: void frameDisplayed(); private: QImage image; ProcessThread *thread; };
方案说明
- 该方案完全实现了“处理一帧、显示一帧”的严格同步,子线程会等主线程真正绘制完成后再处理下一帧,不会出现帧堆积。
- 退出流程不会死锁:设置退出标志后会唤醒等待的子线程,主线程先处理完剩余事件再等待子线程退出,不会出现循环等待。
- 没有野指针风险:所有跨线程传递的图像都做了深拷贝,不需要依赖锁保护共享内存,稳定性更高。
- 修正了原代码中OpenCV BGR格式转QImage RGB格式的颜色错误问题,显示颜色不会异常。
内容的提问来源于stack exchange,提问作者New Coder
相关产品推荐
相关产品推荐

