Qt 中的多线程 02:移动工作对象到子线程中
Qt 中的多线程除了通过继承 QThread
类,然后重写虚函数 run()
之外还有另一种方案:将要执行的复杂操作,放在一个继承自 QObject
的类中,然后使用 QObject::moveToThread(QThread*)
函数将工作对象的移动到创建的子线程中去执行。
一、子线程的实现
工作对象类
该类继承自 QObject
主要是用来实现一些耗时复杂的操作,这些操作可能会阻塞主线程,因此需要在后面将其移到子线程中去执行。
1、MyWork.h
文件
#pragma once
#include <QObject>
class MyWork: public QObject
{
Q_OBJECT
public:
MyWork();
~MyWork();
signals:
/**
* 子线程用来向主线程发送信号.
*/
void SendMsg(QString msg);
public slots:
/**
* 复杂操作在槽函数中实现,然后通过信号槽来与主线程建立联系
* 以触发该复杂的槽函数执行。
*/
void DoSomething();
};
2、MyWork.cpp
文件
#include "MyWork.h"
#include <QThread>
MyWork::MyWork()
{
}
MyWork::~MyWork()
{
}
void MyWork::DoSomething()
{
SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
SendMsg("Begin Work ......");
QThread::sleep(6);
SendMsg("Work Finished.");
}
主线程类
将在主线程类中去创建子线程,和 工作对象,并将工作对象移动到子线程中,然后通过主程序的信号来触发位于子线程中的工作对象,让其执行一些复杂的操作。
1、QThreadTest.h
文件
#pragma once
#include <QtWidgets/QMainWindow>
#include "ui_QThreadTest.h"
#include <QThread>
#include "MyWork.h"
class QThreadTest : public QMainWindow
{
Q_OBJECT
public:
QThreadTest(QWidget *parent = Q_NULLPTR);
~QThreadTest();
signals:
/**
* 主线程通过触发信号,来让子线程开始执行工作.
*/
void BeginWork();
private slots:
/**
* 直接在主线程中进行耗时操作,会直接阻塞主UI线程,导致程序卡死.
*/
void OnBtn01Clicked();
/**
* @brief 通过点击Button来触发对应的信号,然位于子线程中的工作对象开始执行对应的函数
*/
void OnBtn02Clicked();
/**
* @brief 由于子线程不能直接操作UI,所以该函数用于将接受到的子线程内容显示在UI上
* @param [in] msg
*/
void GetWorkMsg(QString msg);
private:
Ui::QThreadTestClass ui;
QThread *subThread; // 创建子线程,用来将任务移动到该线程中去执行
};
2、QThreadTest.cpp
文件
#include "QThreadTest.h"
#include <QPushButton>
QThreadTest::QThreadTest(QWidget *parent)
: QMainWindow(parent)
{
ui.setupUi(this);
subThread = new QThread(this);
// 创建工作对象,不能为其指定父指针,否则后面无法将其移动子线程中去执行
MyWork* myWork = new MyWork();
// 将工作对象移动到子线程中去执行
myWork->moveToThread(subThread);
// 在子线程执行结束之后,将工作对象的资源进行释放
connect(subThread, &QThread::finished, myWork, &MyWork::deleteLater);
// 通过主线程的信号来触发子线程开始执行对应的工作
connect(this, &QThreadTest::BeginWork, myWork, &MyWork::DoSomething);
// 在子线程中的工作对象无法直接修改主线程的UI,因此需要通过信号槽的方式来进行更新
connect(myWork, &MyWork::SendMsg, this, &QThreadTest::GetWorkMsg);
// 启动子线程,此时只是子线程开始执行,但是子线程中的具体工作 DoSomething 还未执行
subThread->start();
connect(ui.btn01, &QPushButton::clicked, this, &QThreadTest::OnBtn01Clicked);
connect(ui.btn02, &QPushButton::clicked, this, &QThreadTest::OnBtn02Clicked);
}
QThreadTest::~QThreadTest()
{
// 创建的线程一定要进行释放,否则关闭程序的时候线程仍然在运行,此时会造成程序Crash
subThread->quit();
subThread->wait();
}
void QThreadTest::OnBtn01Clicked()
{
ui.textBrowser_01->append("Start do something ...");
QThread::sleep(5);
ui.textBrowser_01->append("Finishing this operation.");
}
void QThreadTest::OnBtn02Clicked()
{
ui.textBrowser_02->append(QString("主线程id = %1").arg((int)QThread::currentThreadId()));
emit BeginWork(); // 触发信号,子线程中开始执行工作对象的函数
}
void QThreadTest::GetWorkMsg(QString msg)
{
ui.textBrowser_02->append(msg);
}
该方法的主要流程为:
- 创建一个类,如
MyWork
继承自QObject
,后续会将所有复杂耗时操作都放到该类的槽函数中。 - 在
MyWork
类中将复杂的操作放在槽函数中,后续会通过信号槽的方式来触发子线程执行该操作。 - 在
MyWork
类中定义信号槽,用于子线程向主线程更新数据或信息。 - 在主线程中创建
QThread
对象,和MyWork
类对象,并将 工作对象移动到子线程中。 - 分别建立信号槽:
- 子线程执行完工作后,释放工作对象。(此时子线程可能并未退出)
- 主线程的开始执行信号连接工作对象的槽函数。
- 子线程向主线程发送数据,用来更新UI或同步操作。
- 启动子线程,此时工作对象的复杂操作还未执行。
- 在主线程中某一时机下,触发开始在子线程中执行工作类中的槽函数,此时才会进行复杂操作的执行。
注意
此时阻塞发生在子线程中,而不是主线程中。因此一般一个工作类中可以有多个耗时的操作,这些耗时的操作都会作为槽函数,然后通过信号槽的方式在主线程中去触发该耗时操作。但是在同一个时刻应该只执行一个耗时的操作,如果有多个耗时的操作同时执行,仍然会发生阻塞,只是阻塞在子线程中。后触发的操作需要等待先触发的操作执行完成之后才能进行。所以可以将多个耗时的操作分别放在不同的继承自 QObject
的工作类中,以便后续将其分别放到不同的子线程中执行,从而实现并行的操作。
二、关于MyWork
类的变量、函数以及对象所在的线程
虽然在主线程中将 MyWork
类的对象通过 myWork->moveToThread(subThread)
函数移动到子线程中,但并不是所有的变量、函数或操作都会在子线程中执行,其中一部分的操作仍然位于主线程。
QThreadTest
类的所有对象和成员都是属于主线程,其操作也都是在主线程中。MyWork
类中只有通过信号槽来触发的槽函数是在子线程中执行的,其它的函数仍在主线程中执行。
MyWork
构造函数中的操作位于 主线程
MyWork::MyWork()
{
// 在构造函数中打印当前所在线程的 id
qDebug() << QString(" MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
}
以上代码执行结果如下所示: 构造函数的线程 ID 和 主线程ID相同,所以在 MyWork
类的构造函数中所初始化的变量或操作都属于 主线程,而非 子线程

- 槽函数
DoSomething()
中的操作位于 子线程 中,该函数中初始化的变量也属于子线程
三、子线程中 QTimer
功能
因为 Qt 中事件循环默认在主线程中执行,而子线程中如果没有 exec()
,则会执行之后直接退出,也无法响应子线程的事件循环。
如果在子线程中使用 QTimer
需要注意 QTimer
对象的实例化是否在子线程中进行的,如果是在子线程中进行实例化,则QTimer
对象属于子线程,此时可以在子线程中操作计时器,即调用 QTimer->start()
,否则将无法在子线程中来启动计时器
1. 在子线程中进行实例化 QTimer
如下子线程中调用 QTimer
代码:
MyWork.h
文件
#pragma once
#include <QObject>
#include <QTimer>
class MyWork: public QObject
{
Q_OBJECT
public:
MyWork();
~MyWork();
signals:
/**
* 子线程用来向主线程发送信号.
*/
void SendMsg(QString msg);
public slots:
/**
* 复杂操作在槽函数中实现,然后通过信号槽来与主线程建立联系
* 以触发该复杂的槽函数执行。
*/
void DoSomething();
/**
* @brief 计时器被触发的动作
*/
void RespondTimeOut();
private:
QTimer* m_timer; // 该计时器指针在子线程中进行实例化,则属于子线程;在主线程中实例化,则属于主线程
};
MyWork.cpp
文件
#include "MyWork.h"
#include <QThread>
#include <QDebug>
MyWork::MyWork()
{
qDebug() << QString(" MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
}
MyWork::~MyWork()
{
// 释放 QTimer 对象的资源
m_timer->stop();
delete m_timer;
qDebug() << "Destructor MyWork....";
}
void MyWork::DoSomething()
{
SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
SendMsg("Begin Work ......");
QThread::sleep(3);
SendMsg("Work Finished.");
// 在子线程的函数中实例化,则该对象属于子线程,此时可以在子线程中启动计时器:m_timer->start()
m_timer = new QTimer();
m_timer->setInterval(1000);
connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
m_timer->start();
}
void MyWork::RespondTimeOut()
{
qDebug() << QString(" Timer thread id = %1").arg((int)QThread::currentThreadId());
}
执行结果如下所示:当前计时器响应槽函数所在的线程ID 与 子线程ID相同。

2. 不在子线程中实例化 QTimer
如果将实例化 QTimer
的操作放在 MyWork
的构造函数中,且不指定其父对象,此时 QTimer
的对象将属于主线程,子线程函数中将无法操作该计时器对象。
MyWork::MyWork()
{
qDebug() << QString("MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
// 在构造函数中实例化 QTimer ,且不指定其父对象,此时计时器对象属于主线程。
m_timer = new QTimer();
m_timer->setInterval(1000);
connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
}
void MyWork::DoSomething()
{
SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
SendMsg("Begin Work ......");
QThread::sleep(3);
SendMsg("Work Finished.");
// 此时在子线程函数中启动计时器,将提示无效。
m_timer->start();
}
运行结果如下所示,提示无法从另一个线程中启动计时器。因为此时计时器对象属于主线程,而 DoSomething()
函数操作位于子线程,所以无法从子线程中来启动主线程的计时器。

此时如果指定其父对象为 this
,即当前类MyWork
对象的指针,由于该类通过 moveToThread
移动到子线程中,此时 QTimer
对象 m_timer
也属于子线程,因此可以在子线程函数中来操作该计时器,并使其生效。
MyWork::MyWork()
{
qDebug() << QString("MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
// 在构造函数中实例化 QTimer ,并指定其父对象为当前类对象
m_timer = new QTimer(this);
m_timer->setInterval(1000);
connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
}
void MyWork::DoSomething()
{
SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
SendMsg("Begin Work ......");
QThread::sleep(3);
SendMsg("Work Finished.");
// 此时在子线程函数中可以启动计时器
m_timer->start();
}
运行结果如下:

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)