Qt 多线程
Qt 提供两种多线程编程范式:基于事件循环的 QThread 模型,以及更高层封装的 QtConcurrent API。
一、Qt 多线程与信号槽机制
Qt 提供基于事件驱动的多线程编程模型,其核心在于 QThread 类与信号槽(Signal & Slot)通信机制。该机制专门设计用于处理跨线程通信,能够自动管理线程间的数据同步与函数调用。
1.1 核心原理:跨线程信号槽连接
在进行跨线程通信时,Qt 的信号槽机制扮演了关键角色。槽函数总是由接收该信号的对象所在的线程来执行。
- 自动线程安全:当信号槽连接被正确建立后,开发者无需手动加锁,系统会保证槽函数在接收者对象所属的线程上下文中被安全调用。
- 执行模式 :支持两种连接方式:
Qt::DirectConnection:槽函数在发送信号者的线程中立即执行。Qt::QueuedConnection:槽函数被放入接收者线程的事件队列中,由该线程的事件循环调度执行。
1.1.1 默认跨线程连接方式
对于在不同线程中的对象之间建立的信号槽连接,Qt 的默认行为是 Qt::QueuedConnection。这意味着:
- 发送信号的线程会将信号及其参数封装为一个事件(
QMetaCallEvent)。 - 该事件被放入接收者对象所属线程的事件队列。
- 当接收者线程运行到事件循环(
QThread::exec())时,会取出并处理该事件,从而执行对应的槽函数。
关键前提 :接收者线程必须运行事件循环,QueuedConnection 才能正常工作。
1.2 QThread 的正确使用范式
QThread 本身是 Qt 提供的线程管理类,核心原则是: QThread 管理的是线程的上下文,而非业务逻辑本身。
1.3 代码示例:Worker 类定义
cpp
#include "Worker.h"
Worker::Worker(QObject* p) :QObject(p)
{
}
void Worker::Register(const std::function<void(const Info& info)>& func)
{
m_func = func;
}
void Worker::doWork() {
for (int i = 1; i <= 10; i++) {
QThread::sleep(1);
emit SendProcess((i / 10.0 * 100));
if(m_func)
m_func(Info(std::to_string((i / 10.0 * 100)).c_str()));
emit SendProcessMsg(Info(std::to_string((i / 10.0 * 100)).c_str()));
}
emit finished();
}
1.4 代码示例:Worker 类实现
cpp
#pragma once
#include <qobject.h>
#include <qthread.h>
struct Info {
QString msg;
Info(const QString& _msg) :msg(_msg) {};
Info() {};
Info(const Info& _info)
{
this->msg = _info.msg;
}
};
class Worker:public QObject
{
Q_OBJECT
public:
explicit Worker(QObject* p = nullptr);
public slots:
void doWork();
private:
std::function<void(const Info& info)> m_func;
public:
void Register(const std::function<void(const Info& info)>& func);
signals:
void SendProcess(int i);
void SendProcessMsg(const Info& info);
void finished();
};
1.5 代码示例:主窗口中的线程启动与管理
cpp
#include "QThreadTest.h"
QThreadTest::QThreadTest(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
QHBoxLayout* hbLayout = new QHBoxLayout(ui.centralWidget);
mp_lb = new QLabel(QString::fromLocal8Bit("开始计时"), ui.centralWidget);
hbLayout->addWidget(mp_lb);
ui.centralWidget->setLayout(hbLayout);
QThread* pThread = new QThread;
Worker* pWorker = new Worker;//这里需要注意下,不能设置父节点是this,否则Worker无法moveToThread到子线程上
pWorker->moveToThread(pThread);
connect(pThread, &QThread::started, pWorker, &Worker::doWork);
connect(pWorker, &Worker::SendProcess, [&](int i) {
mp_lb->setText(QString::fromLocal8Bit("当前进度:" + std::to_string(i) + "%"));
});
connect(pWorker,&Worker::finished, pThread, &QThread::quit);
connect(pWorker, &Worker::finished, pWorker, &QThread::deleteLater);
connect(pWorker, &Worker::finished, pThread, &QThread::deleteLater);
pThread->start();
}
QThreadTest::~QThreadTest()
{}
二、Qt Concurrent 进阶 API(无需手动管理线程)
Qt Concurrent 是 Qt 提供的高级封装 ,开发者无需手动创建 QThread,通常只需一行代码即可实现跨线程操作,适合处理简单的耗时任务。
2.1 核心优势
2.2 适用场景
2.3 跨线程访问 UI:QMetaObject::invokeMethod
采用 QMetaObject::invokeMethod 方法可以实现跨线程访问 UI。需要注意 ,跨线程访问 UI 时,QMetaObject::invokeMethod 中的连接参数必须设置为 Qt::QueuedConnection。
QMetaObject::invokeMethod 是 Qt 元对象系统 提供的跨线程调用方法,仅用于 QObject 对象 ,其核心作用是:让一段代码在「目标对象所属的线程」中执行。
若想让代码在 主线程(用于更新 UI) 中执行,第一个参数必须是主线程中的 QObject 对象(例如窗口 this、UI 控件)。
cpp
// 目标:在主线程安全地更新UI
// 第一个参数传递主线程对象(this / mp_lb)→ 预执行的代码块将在主线程中执行
QMetaObject::invokeMethod(this, [=]() {
mp_lb->setText("主线程安全更新UI");
}, Qt::QueuedConnection);
被 invokeMethod 调用的对象必须满足以下条件:
cpp
// 自定义类型
struct Info {
QString msg;
Info(const QString& _msg) :msg(_msg) {};
Info() {};
Info(const Info& _info)
{
this->msg = _info.msg;
}
};
// 主线程初始化时注册该元类型
qRegisterMetaType<Info>("Info");
使用 invokeMethod 调用既有方法时,该方法需要:
cpp
Q_INVOKABLE void UpdateLbInfo(const Info& info);
2.4 代码示例:QtConcurrent 结合 invokeMethod
cpp
#include "QThreadTest.h"
QThreadTest::QThreadTest(QWidget *parent)
: QMainWindow(parent)
{
qRegisterMetaType<Info>("Info");
ui.setupUi(this);
QHBoxLayout* hbLayout = new QHBoxLayout(ui.centralWidget);
mp_lb = new QLabel(QString::fromLocal8Bit("开始计时"), ui.centralWidget);
hbLayout->addWidget(mp_lb);
ui.centralWidget->setLayout(hbLayout);
QtConcurrent::run([&]() {
Worker worker;
connect(&worker, &Worker::SendProcessMsg, [&](const Info& info) {
QMetaObject::invokeMethod(this, &QThreadTest::UpdateLbInfo, Qt::QueuedConnection, info);
});
worker.doWork();
});
}
QThreadTest::~QThreadTest()
{}
void QThreadTest::UpdateLbInfo(const Info& info)
{
mp_lb->setText(QString::fromLocal8Bit("当前进度:" + info.msg.toStdString() + "%"));
}
-
发射信号的线程将信号事件放入接收者线程的事件队列中。
-
业务逻辑的封装 :开发者不应将业务逻辑直接写在
QThread子类的run()方法中(旧式用法)。推荐的做法是:- 将业务逻辑封装在一个独立的
Worker对象(继承自QObject)中。 - 将该
Worker对象通过moveToThread()方法移动到QThread对象所代表的新线程中。 - 通过信号槽机制,从主线程(或其他线程)向
Worker对象发送指令,或接收其发出的进度、完成等信号。
- 自动管理线程池,无需编写
moveToThread、connect、deleteLater等底层代码。 - 代码量极少,上手门槛低。
- 短期、简单的耗时操作,例如计算密集型任务、文件读取、网络请求等。
- 不需要长期持有线程的场景。
- 必须是 QObject 的派生类。
- 类声明中必须包含
Q_OBJECT宏。 - 如需传递自定义类型参数,必须先行注册该类型的元类型。
- 使用
Q_INVOKABLE宏声明的普通成员函数;或者 - 是
public slot槽函数。
- 将业务逻辑封装在一个独立的