Qt 多线程

Qt 多线程

Qt 提供两种多线程编程范式:基于事件循环的 QThread 模型,以及更高层封装的 QtConcurrent API。

一、Qt 多线程与信号槽机制

Qt 提供基于事件驱动的多线程编程模型,其核心在于 QThread 类与信号槽(Signal & Slot)通信机制。该机制专门设计用于处理跨线程通信,能够自动管理线程间的数据同步与函数调用。

1.1 核心原理:跨线程信号槽连接

在进行跨线程通信时,Qt 的信号槽机制扮演了关键角色。槽函数总是由接收该信号的对象所在的线程来执行。

  • 自动线程安全:当信号槽连接被正确建立后,开发者无需手动加锁,系统会保证槽函数在接收者对象所属的线程上下文中被安全调用。
  • 执行模式 :支持两种连接方式:
    • Qt::DirectConnection:槽函数在发送信号者的线程中立即执行。
    • Qt::QueuedConnection:槽函数被放入接收者线程的事件队列中,由该线程的事件循环调度执行。
1.1.1 默认跨线程连接方式

对于在不同线程中的对象之间建立的信号槽连接,Qt 的默认行为是 Qt::QueuedConnection。这意味着:

  1. 发送信号的线程会将信号及其参数封装为一个事件(QMetaCallEvent)。
  2. 该事件被放入接收者对象所属线程的事件队列
  3. 当接收者线程运行到事件循环(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() 方法中(旧式用法)。推荐的做法是:

    1. 将业务逻辑封装在一个独立的 Worker 对象(继承自 QObject)中。
    2. 将该 Worker 对象通过 moveToThread() 方法移动到 QThread 对象所代表的新线程中。
    3. 通过信号槽机制,从主线程(或其他线程)向 Worker 对象发送指令,或接收其发出的进度、完成等信号。
    • 自动管理线程池,无需编写 moveToThreadconnectdeleteLater 等底层代码。
    • 代码量极少,上手门槛低。
    • 短期、简单的耗时操作,例如计算密集型任务、文件读取、网络请求等。
    • 不需要长期持有线程的场景。
    1. 必须是 QObject 的派生类
    2. 类声明中必须包含 Q_OBJECT
    3. 如需传递自定义类型参数,必须先行注册该类型的元类型。
    • 使用 Q_INVOKABLE 宏声明的普通成员函数;或者
    • public slot 槽函数。
相关推荐
handler017 小时前
从源码到二进制:深度拆解 Linux 下 C 程序的编译与链接全流程
linux·c语言·开发语言·c++·笔记·学习
t***5447 小时前
如何在Dev-C++中使用Clang编译器
开发语言·c++
Qbw20048 小时前
【Linux】进程地址空间
linux·c++
6Hzlia9 小时前
【Hot 100 刷题计划】 LeetCode 739. 每日温度 | C++ 逆序单调栈
c++·算法·leetcode
良木生香9 小时前
【C++初阶】:STL——String从入门到应用完全指南(1)
c语言·开发语言·数据结构·c++·算法
Kurisu_红莉栖10 小时前
c++复习——const,static字
c++
czxyvX10 小时前
1-Qt概述
c++·qt
齐鲁大虾10 小时前
新人编程语言选择指南
javascript·c++·python·c#
CoderMeijun11 小时前
C++ 多线程进阶:Lambda、条件变量与死锁
c++·多线程·条件变量·lambda·死锁·生产者消费者
unicrom_深圳市由你创科技11 小时前
上位机开发常用的语言 / 框架有哪些?
c++·python·c#