You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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参数,上述退出卡死问题不会复现,但无法实现预期的显示同步效果。
根因分析

卡死的核心是死锁,逻辑如下:

  1. Qt::BlockingQueuedConnection的机制是:发送信号的线程会一直阻塞,直到槽函数在接收者所属线程执行完成才会返回。
  2. 程序退出时,主线程执行析构函数,调用thread->wait()后会一直阻塞,等待子线程执行完毕退出,此时主线程不会再处理任何事件循环中的任务。
  3. 如果子线程此时刚好执行到emit processCompleted(&data),就会阻塞等待主线程执行对应的槽函数;但主线程卡在wait()根本不会处理事件、执行槽,子线程就永远无法继续运行、退出循环。
  4. 主线程等子线程退出,子线程等主线程执行槽,形成循环等待,就卡死了。

另外代码还有三个隐藏问题:

  • 重写QThread::run()后内部是while循环,没有调用exec()开启线程事件循环,thread->quit()调用完全无效。
  • 跨线程传递Mat裸指针,且构造QImage时直接引用Mat的内存地址不做深拷贝,就算不卡死,子线程后续覆盖data成员时,主线程的QImage会访问野指针,触发崩溃或显示花屏。
  • 锁粒度过大:锁从读取图像开始持有,直到信号发送、槽执行完成才释放,持有锁期间跨线程阻塞,进一步放大死锁概率。
  • 原代码没有做颜色空间转换:OpenCV默认读取的彩色图是BGR格式,直接构造RGB888格式的QImage会出现颜色显示错误。
修复方案

不需要依赖Qt::BlockingQueuedConnection也能实现可靠的帧同步,同时彻底避免死锁,修改点如下:

  1. 移除无效的quit()调用,修正退出流程:析构时先设置退出标志,先处理完主线程所有待处理事件,再等待子线程退出,避免子线程卡在信号发送阶段。
  2. 跨线程传值而非传裸指针:信号传递Mat对象(Mat本身带引用计数,传值开销极低),主线程收到后立刻做深拷贝转成QImage,不需要依赖锁保护共享的data成员,彻底消除野指针风险。
  3. 缩小锁范围,移除不必要的锁:子线程把处理后的Mat作为局部变量通过信号传出,不需要锁保护共享变量;同步逻辑用QWaitCondition实现,锁仅覆盖等待条件判断的极短片段。
  4. 用显式帧应答实现同步:如果需要严格保证“处理完一帧、显示完一帧再处理下一帧”的同步逻辑,增加一个主线程到子线程的应答信号,主线程完成绘制后发信号通知子线程可以处理下一帧,比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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 04:01:13