Qt多线程编程的QThread类(详细)
本⽂结构如下:
1. 概述
2. 优雅的开始我们的多线程编程之旅
1. 我们该把耗时代码放在哪⾥?
2. 再谈 moveToThread()
3. 启动线程前的准备⼯作
1. 开多少个线程⽐较合适?
2. 设置栈⼤⼩
4. 启动线程/退出线程
1. 启动线程
2. 优雅的退出线程
5. 操作运⾏中的线程
1. 获取状态
1. 运⾏状态
2. 线程标识
3. 更为精细的事件处理
2. 操作线程
1. 安全退出线程必备函数:wait()
2. 线程间的礼让⾏为
3. 线程的中断标志位
6. 为每个线程提供独⽴数据
7. 附:所有函数
1. 概述
在阅读本⽂之前,你需要了解进程和线程相关的知识,详情参考《》。
在很多⽂章中,⼈们倾向于把 QThread 当成线程的实体,区区创建⼀个 QThread 类对象就被认为是开了⼀个新线程。当然这种讨巧的看法似乎能快速的让我们⼊门,但是只要深⼊多线程编程领域后就会发现这种看法越来越站不住脚,甚⾄编写的代码脱离我们的控制,代码越写越复杂。最典型的问题就是“明明把耗时操作代码放⼊了新线程,可实际仍在旧线程中运⾏”。造成这种情况的根源在于继承 QThread 类,并在 run() 函数中塞⼊耗时操作代码。
追溯历史,在 Qt 4.4 版本以前的 QThread 类是个抽象类,要想编写多线程代码唯⼀的做法就是继承 QThread 类。但是之后的版本
中,Qt 库完善了线程的亲和性以及信号槽机制,我们有了更为优雅的使⽤线程的⽅式,即 QObject::moveToThread()。这也是官⽅推荐的做法,遗憾的是⽹上⼤部分教程没有跟上技术的进步,依然采⽤ run() 这种腐朽的⽅式来编写多线程程序。
2. 优雅的开始我们的多线程编程之旅
在 Qt 4.4 版本后,之所以 Qt 官⽅对 QThread 类进⾏了⼤⼑阔斧地改⾰,我认为这是想让多线程编程更加符合 C++ 语⾔的「⾯向对象」特性。继承的本意是扩展基类的功能,所以继承 QThread 并把耗时操作代码塞⼊ run() 函数中的做法怎么看都感觉不伦不类。
2.1 我们该把耗时代码放在哪⾥?
暂时不考虑多线程,先思考这样⼀个问题:想想我们平时会把耗时操作代码放在哪⾥?⼀个类中。那么有了多线程后,难道我们要把这段代码从类中剥离出来单独放到某个地⽅吗?显然这是很糟糕的做法。QObject 中的 moveToThread() 函数可以在不破坏类结构的前提下依然可以在新线程中运⾏。
假设现在我们有个 QObject 的⼦类 Worker,这个类有个成员函数 doSomething(),该函数中运⾏的代码⾮常耗时。此时我要做的就是将这个类对象“移动”到新线程⾥,这样 Worker 的所有成员函数就可以在新线程中运⾏了。那么如何触发这些函数的运⾏呢?信号槽。在主线程⾥需要有个 signal 信号来关联并触发 Worker 的成员函数,与此同时 Worker 类中也应该有个 signal 信号⽤于向外界发送运⾏的结果。这样思路就清晰了,Worker 类需要有个槽函数⽤于执⾏外界的命令,还需要有个信号来向外界发送结果。如下列代码:
Worker.h:
// Worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
signals:
void resultReady(const QString &str); // 向外界发送结果
public slots:
void on_doSomething(); // 耗时操作
};
#endif // WORKER_H
Worker.cpp:
// Worker.cpp
#include "worker.h"
#include <QDebug>
#include <QThread>
Worker::Worker(QObject *parent) : QObject(parent)
{
}
void Worker::on_doSomething()
{
qDebug() << "I'm working in thread:" << QThread::currentThreadId();
emit resultReady("Hello");
}
Controller.h:
// Controller.h
#ifndef CONTROLLER_H
#define CONTROLLER_H
#include <QObject>
#include <QThread>
#include "worker.h"
class Controller : public QObject
{
Q_OBJECT
public:
explicit Controller(QObject *parent = nullptr);
~Controller();
void start();
signals:
void startRunning(); // ⽤于触发新线程中的耗时操作函数
public slots:
void on_receivResult(const QString &str); // 接收新线程中的结果
private:
QThread m_workThread;
Worker *m_worker;
};
#endif // CONTROLLER_H
在作为“外界”的 Controller 类中,由于要发送命令与接收结果,因此同样是有两个成员:startRunning() 信号⽤于启动 Worker 类的耗时函数运⾏,on_receivResult() 槽函数⽤于接收新线程的运⾏结果。注意别和 Worker 类的两个成员搞混了,在本例中信号对应着槽,即“外界”的信号触发“新线程”的槽,“外界”的槽接收“新线程”的信号结果。
Controller.cpp:
// Controller.cpp
#include "controller.h"
#include <QThread>
#include <QDebug>
Controller::Controller(QObject *parent) : QObject(parent)
{
qDebug() << "Controller's thread is :" << QThread::currentThreadId();
m_worker = new Worker();
m_worker->moveToThread(&m_workThread);
connect(this, &Controller::startRunning, m_worker, &Worker::on_doSomething);
connect(&m_workThread, &QThread::finished, m_worker, &QObject::deleteLater);
connect(m_worker, &Worker::resultReady, this, &Controller::on_receivResult);
m_workThread.start();
}
Controller::~Controller()
{
m_workThread.quit();
m_workThread.wait();
}
void Controller::start()
{
emit startRunning();
}
void Controller::on_receivResult(const QString &str)
{
qDebug() << str;
}
在 Controller 类的实现⾥,⾸先实例化⼀个 Worker 对象并把它“移动”到新线程中,然后就是在新线程启
动前将双⽅的信号槽连接起来。同 Worker 类⼀样,为了体现是在不同线程中执⾏的,我们在构造函数中打印当前线程 ID。
main.cpp:
// main.cpp
#include <QCoreApplication>thread技术
#include <QThread>
#include <QDebug>
#include "controller.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
qDebug() << "The main threadID is :" << QThread::currentThreadId();
Controller controller;
controller.start();
();
}
在 main.cpp 中我们实例化⼀个 Controller 对象,并运⾏ start() 成员函数发射出信号来触发 Worker 类的耗时操作函数。来看看运⾏结果:
从结果可以看出,Worker 类对象的成员函数是在新线程中运⾏的。⽽ Controller 对象是在主线程中被创建,因此它就⾪属于主线程。
2.2 再谈 moveToThread()
“移动到新线程”是⼀个很形象的描述,作为⼊门的认知是可以的,但是它的本质是改变线程亲和性(也叫关联性)。为什么要强调这⼀点?这是因为如果你天真的认为 Worker 类对象整体都移动到新线程中去了,那么你就会本能的认为 Worker 类对象的控制权是由新线程所属,然⽽事实并不是如此。「在哪创建就属于哪」这句话放在任何地⽅都是适⽤的。⽐如上⼀节的例⼦中,Worker 类对象是在Controller 类中创建并初始化,因此该对象是属于主线程的。⽽ moveToThread() 函数的作⽤是将槽函数在指定的线程中被调⽤。当然,在新线程中调⽤函数的前提是该线程已经启动处于就绪状态,所以在上⼀节的 Controller 构造函数中,我们把各种信号槽连接起来后就可以启动新线程了。
使⽤ moveToThread() 有⼀些需要注意的地⽅,⾸先就是类对象不能有⽗对象,否则⽆法将该对象“移动”到新线程。如果类对象保存在栈上,⾃然销毁由操作系统⾃动完成;如果是保存在堆上,没有⽗对象的指针要想正常销毁,需要将线程的 finished() 信号关联到QObject 的deleteLater() 让其在正确的时机被销毁。其次是该对象⼀旦“移动”到新线程,那么该对象中的计时器(如果有 QTimer 等计时器成员变量)将重新启动。不是所有的场景都会遇到这两种情况,但是记住这两个⾏为特征可以避免踩坑。
3. 启动线程前的准备⼯作
3.1 开多少个线程⽐较合适?
说“开线程”其实是不准确的,这种事⼉只有操作系统才能做,我们所能做的是管理其中⼀个线程。⽆论是 QThread thread 还是QThread *thread,创建出来的对象仅仅是作为操作系统线程的接⼝,⽤这个接⼝可以对线程进⾏⼀些操作。虽然这样说不准确,但下⽂我们仍以“开线程”的说法,只是为了表述⽅便。作为⼊门教程,能在主线程之外“开”⼀个线程就已经够了,那么讲解“开”多个线程的内容实在没有必要。本节的⽬的是想在叩开多线程⼤门的同时能向⾥望⼀望多线程领域的世界,就当是抛砖引⽟吧。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论