前言
在面试Qt相关岗位时,如何使用线程是一个相当高频的问题。而即便已经有了一些开发经验,这个问题依旧可能回答得不出彩,只能算应付过去。
事实上,在Qt中使用线程,主要有四种能够回答的方案,分别是直接继承QThread、moveToThread、Qt::ConCurrent和QThraedPool。
我打算依次将这些内容全部写明白,算是一个总结。
一、QThread
QThread是Qt中的线程类,也是实现线程最为关键核心的类。它的使用较为简单,主要是通过start()、stop()接口来开启和结束线程,然后通过重载run()函数来实现我们想要完成的任务。
说到这里,我想先提及一个概念。那就是什么样的任务需要放到线程中执行?
我的观点是,只要会造成主线程阻塞,哪怕只是短短几百毫秒,都有迁移到线程中执行的意义。那是因为对于客户操作体验来说,流畅性是非常重要的考量。在某些情况下,界面的阻塞允许被接受,但它绝对有优化的空间。比如我们点击按钮后,试图通过网络完成身份登录校验,或者有数据需要写入数据库。此时出现的阻塞,有部分客户勉强接受,有一些客户就会提出优化建议了。
以上是造成短阻塞的任务,通常是希望通过线程来达到异步操作,避免阻塞主线程,进而可能阻塞UI。
然而还有另一种常见情况,长时间的阻塞,或者换句话说,是丢到while循环中不断执行的操作。最常见的就是音视频解码、网络流数据获取这些场景。对于这些任务,放到线程中让它长时间执行显然是必须的决策级方案,但有一些问题也会随之而来。比如线程不会完成任务后自动退出,我们必须小心处理结束线程的流程操作,以及相关资源的析构。再来,就是跨线程的通信问题,while循环通常涉及数据采集,采集到数据后,往往需要发送到外部进行其他的处理,例如图像帧的渲染。在Qt中,通常会使用信号槽来实现这些功能,这有涉及到所谓的第五位参数,ConnectType连接方式。这也是一个面试高频问题,之后我会详细探讨。
二、直接继承QThread
在本节中,我们先实现直接继承QThread的线程使用,这也是最基础和适合新手的使用。
先介绍一下代码结构。我会在默认的MainWindow界面类中,添加成组的开始、结束按钮,用来测试每一组不同的线程使用方式。
然后,我会定义不同的线程类来实现这些功能。
直接放上代码:
WorkerThread类:
cpp
#ifndef WORKERTHREAD_H
#define WORKERTHREAD_H
#include <QThread>
#include <atomic>
class WorkerThread : public QThread
{
Q_OBJECT
public:
explicit WorkerThread(QObject *parent = nullptr);
~WorkerThread() override;
void thread_start();
void thread_stop();
protected:
void run() override;
private:
std::atomic<bool> m_stopFlag = false;
};
#endif // WORKERTHREAD_H
cpp
#include "WorkerThread.h"
#include <QDebug>
#include <QThread>
#include <QDateTime>
WorkerThread::WorkerThread(QObject *parent)
: QThread(parent)
{
qDebug() << "WorkerThread created in" << QThread::currentThread();
m_stopFlag = false;
}
WorkerThread::~WorkerThread()
{
m_stopFlag = true;
// 等待线程结束(避免在run()未退出时销毁对象)
wait();
qDebug() << "WorkerThread destroyed";
}
void WorkerThread::thread_start()
{
m_stopFlag = false;
this->start();
}
void WorkerThread::thread_stop()
{
qDebug() << "WorkerThread stopped by flag";
m_stopFlag = true;
}
void WorkerThread::run()
{
qDebug() << "WorkerThread started in" << QThread::currentThread();
while (!m_stopFlag.load()) {
qDebug() << "Worker thread running... (loop)"<<QDateTime::currentDateTime();
QThread::msleep(500); // 模拟工作(500ms间隔)
}
m_stopFlag = true;
// 这里自动返回QThread的finished信号
// finished();
}
这个类中,通过重载run函数来实现线程任务,这里主要是通过一个while来定时每半秒打印一次当前时间。
既然涉及到while,就需要处理中断循环的问题,这里直接使用了bool标志m_stopFlag,以及配套的thread_start和thread_stop接口来进行操作。
这里我们避免直接使用QThread的stop()接口来结束线程,不然会有问题。为了确保线程安全等问题,这里的bool类型改用了原子操作std::atomic。其实不用的话,逻辑上也没啥大问题。
当m_stopFlag被置真后,run()会执行结束,此时QThread会自动发送finish信号,无需我们手动发送。
接下来,就是外部的使用和相关信号槽的连接了。
这是界面类MainWindow的部分代码,其实也就两个开始和结束的按钮点击槽函数,以及一个WorkerThread线程类的指针。
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "workerthread.h"
#include "workermanager.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
void on_btn_thread_start_1_clicked();
void on_btn_thread_stop_1_clicked();
private:
Ui::MainWindow *ui;
// 线程1
WorkerThread * p_thread_1 = nullptr;
};
#endif // MAINWINDOW_H
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_btn_thread_start_1_clicked()
{
if(!p_thread_1){
p_thread_1 = new WorkerThread();
connect(p_thread_1, &WorkerThread::finished, this, [this](){
// 理论上就可以安全释放线程类了
p_thread_1->deleteLater();
p_thread_1 = nullptr;
});
// p_thread_1->start();
p_thread_1->thread_start();
}
}
void MainWindow::on_btn_thread_stop_1_clicked()
{
if(p_thread_1){
if(p_thread_1->isRunning())
{
// 如果线程正在运行,就可以停止
p_thread_1->thread_stop();
// 后续的析构放在创建时绑定的信号槽,这样最安全
}
}
}
这里采用开始结束时,动态创建和析构资源的方式。
cpp
connect(p_thread_1, &WorkerThread::finished, this, [=](){
// 理论上就可以安全释放线程类了
p_thread_1->deleteLater();
p_thread_1 = nullptr;
});
使用时,我们要关注析构资源的时机。当调用了p_thread_1->thread_stop();来停止线程后,线程结束的时机存在一定延时,也就是异步的。这里的延时是不确定的,不要抱有通过定时器延时一秒钟后,直接delete p_thread_1的想法,这可能会导致程序崩溃。
而且,我们应该习惯使用qt的deleteLater来析构类对象,它会自动将析构需求添加到事件队列中,在合适的时机进行释放。并且多次调用deleteLater也是安全的。
至此,就是直接继承QThread来使用线程的方式了。
最后,运行代码,依次点击开始和结束按钮。


安全析构,完美!
三、总结
直接继承QThread的线程使用无疑十分简单和方便,但有一个最为致命的问题,那就是业务代码和线程实现高度耦合。说的直白点,你是将业务代码嵌合在QThread的run函数中,只有实现了线程,才能跑业务代码。
问题来了,如果我希望在主线程中直接跑这段业务代码,我该怎么办呢?这段业务代码完全无法复用,你需要先启动线程,才可以运行这段代码。这面对复杂需求,显然是不合适的设计。
于是将业务代码和线程实现剥离开来,就成了更优的选择。无需将业务代码耦合在run函数内,自然也不需要继承QThread来实现自定义的线程类。
所以,自定义业务类+QThread的搭配使用,就成了qt中主流的使用方式,这也是qt官方推荐的使用方式。
下一节,我将介绍moveToThread的实现。