1.吐槽
QThread类提供了一种平台无关的方法对线程进行管理。但对于QThread类的熟练使用,即使是从事Qt开发多年的程序猿们,往往也会踩雷、入坑。总之:QThread类不好用、如果对该类理解不透,很容易导致程序崩溃。本人强烈建议:利用C++进行线程开发,请用STL中的std::thread,std::thread进行线程开发非常简单,不易出错。关于std::thread的使用 ,请参考如下链接:
- 《C++11线程管理基础》。
- 《c++11 thread类的简单使用》。
- 《c++11仔细地将参数传递给线程std::thread》。
- 《C++11向线程函数传递参数》。
- 《C++11多线程thread参数传递问题》。
- 《C++11中线程所有权转移分析》。
2.预备知识
2.1.Object对象和线程的密切关系
Qt中所有QObject对象都和线程有密切关系,或者说所有QObject对象都位于某个线程对象、依附于某个线程对象。当某个QObject对象收到queued signal(通过connect以方式Qt::QueuedConnection连接的信号)或posted events(通过postEvent发送的异步事件)时,槽函数或事件处理函数将会在QObject对象依附的线程内执行。
说明:如果一个QObject对象没有位于任何线程,即没有附属于任何线程,就是说QObject对象的thread()方法返回nullptr,或者QObject对象所在线程没有运行任何事件循环,则QObject对象不会收到queued signal和posted events。QObject对象和其依附的线程关系就类似地球上的人和国家的关系。地球上的人必须属于某个国家,即地球人必须有国籍;只有有国籍,该地球人才能享受到这个国家的待遇,如:社会保险、救济金,身份证等,当然也可能有些人没有任何国籍,那么该人就不会享受到任何国家的权利和应尽义务。
QObject对象默认依附于创建它的线程内。一个QObject对象所在、所依附的线程对象可以通过QObject类的QObject::thread()方法来查询,如下为该方法:
QThread *QObject::thread() const
Qt官方对该方法的解释如下:
Returns the thread in which the object lives.
即返回该对象依附的线程对象。可以通过QObject类的moveToThread方法来改变QObject类和线程的依附关系。
所有对象依附的线程必须和它们的父对象依附的线程是同一个线程,因此:
- 如果两个QObject对象分别依附于不同的线程,对它们之间setParent()函数,从而设置父子关系会失败。
- 当将QObject对象通过moveToThread函数移动到另外一个线程,则QObject对象的所有孩子对象也会自动被移动到该线程。
- 当QObject对象有父时,调用QObject对象的moveToThread函数会失败。
- QThread::run()函数会产生一个新的子线程(为便于后文描述,称为子线程B),如果 QObject对象是在QThread::run()函数内部创建的,则 QObject对象依附于B,则在B线程中创建的QObject对象不会变为QThread的孩子,因为QThread对象不是B线程创建的。也就是说在QThread::run()内部创建的QObject对象和QThread对象依附于不同的线程对象,前者依附于B线程,后者依附于创建它的线程。
说明:QObject对象的成员变量不会自动变为QObject对象的孩子。父子关系的建立必须通过下述方法中某一种形成:
- 通过将QObject对象的指针传给孩子的构造函数。
- 通过将QObject对象作为setParent函数的参数传入。
没用通过上述方法中某一种建立父子关系,则调用moveToThread函数后,对象的成员变量依然依附于原来创建该对象的线程。
2.2.改变QObject类对象的线程依附关系
QObject类有moveToThread函数,如下:
void QObject::moveToThread(QThread *targetThread)
该方法改变QObject类对象及该对象下的所有子对象的线程依附关系到参数targetThread线程对象上。比如:如果a是一个QObject类对象,b是a的孩子对象,a对象是在线程th1中创建的,则调用如下代码后:
QThread *c = ... ;// c是另外一个线程对象
a.moveToThread(c);
a及a的孩子b就不再依附于th1,转而依附于线程c了。此后,a中的事件处理将会在线程c中进行,而不再是在原来的th1线程中进行。
说明:如果QObject类对象有父,则它不能被移动,上例中对b调用如下代码,则不会改变b依附的线程到c上,b中的事件处理依然在线程th1中进行,而不是在c线程中进行。:
QThread *c = ... ;// c是另外一个线程对象
b.moveToThread(c);
利用如下代码:
QApplication::instance();
可以查询指向当前应用程序的指针;然后利用如下代码查询当前应用程序依附的线程对象:
QApplication::thread();
如下代码:
myObject->moveToThread(QApplication::instance()->thread());
可以将QObject类型的myObject对象依附的线程移动到当前应用程序所在的线程上来。如下:
myObject->moveToThread(nullptr);
如果moveToThread的参数targetThread为nullptr,则调用moveToThread函数的对象myObject及其子对象的所有事件都会停止,因为myObject不再依附任何线程对象。
上述对myObject以非nullptr参数调用moveToThread函数,则myObject对象上的所有正处于激活状态的定时器(QTimer、QBaseTimer等)都会被重置。myObject对象关联的定时器首先会在当前线程(创建myObject对象的线程)停止,然后以相同的定时器间隔在moveToThread的参数targetThread表示的线程被重新启动。如果一直不间断的在线程之间调用moveToThread,则定时器事件会无限期地被延迟。
当改变对象的线程依附关系之前, QEvent::ThreadChange事件会发送到该对象上,你能够处理该事件,以执行某些特定的操作。注意:改变对象的线程依附关系后,所有发送到该对象上的事件,将都会在targetThread表示的线程内被处理,而不再是在该对象被创建的线程内被处理。如果moveToThread的参数targetThread为nullptr,则调用moveToThread的对象及其所有孩子的所有事件都不会被处理、激发,因为没有任何线程和该对象及其孩子关联。
注意:moveToThread函数不是线程安全的;该函数仅仅能从当前线程“压入”一个对象到另外一个线程,而不能从任意一个线程“取出”一个对象到当前线程。不过这个规则有个例外:那就是不依附任何线程的对象能被“取出”到当前线程。
3.QThread类
QThread类提供了一种平台无关的方法对线程进行管理,QThread类能在程序内对一个线程进行控制。QThread类的run方法被调用后,就会生成子线程并且该子线程就开始执行。默认情况下,QThread类的run方法通过调用exec()函数开启事件循环,且在线程内部运行事件循环。你可以构建一个工作者对象worker objects,然后通过QObject::moveToThread()方法将工作者对象worker objects移动到某个线程上,即前面说的依附到某个线程对象上。如下代码:
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &result);
};
class Controller : public QObject
{
Q_OBJECT
QThread workerThread;
public:
Controller() {
Worker *worker = new Worker;
worker->moveToThread(&workerThread);
connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
connect(this, &Controller::operate, worker, &Worker::doWork);
connect(worker, &Worker::resultReady, this, &Controller::handleResults);
workerThread.start();
}
~Controller() {
workerThread.quit();
workerThread.wait();
}
public slots:
void handleResults(const QString &);
signals:
void operate(const QString &);
};
在上面的代码中,因为Worker类对象worker通过moveToThread函数将其依附的线程从创建它的线程移动到了workerThread线程,所以Worker类对象worker的槽函数doWork将会在workerThread线程中执行,而不再是在原来创建worker对象的线程执行。尽管如此,你依然可以自由地连接Worker类对象worker的槽函数到任何对象的任何信号。得益于Qt的
queued connections技术(即通过connect以Qt::QueuedConnection为参数连接信号)的使用,在不同的线程之间连接信号和槽是安全的。
另外一种使代码运行在单一子线程的方法是子类化QThread类,然后重新实现run()方法,如下:
class WorkerThread : public QThread
{
Q_OBJECT
void run() override {
QString result;
/* ... here is the expensive or blocking operation ... */
emit resultReady(result);
}
signals:
void resultReady(const QString &s);
};
void MyObject::startWorkInAThread()
{
WorkerThread *workerThread = new WorkerThread(this);
connect(workerThread, &WorkerThread::resultReady, this, &MyObject::handleResults);
connect(workerThread, &WorkerThread::finished, workerThread, &QObject::deleteLater);
workerThread->start();
}
在这个例子中,当run函数执行完返回时,线程就会退出,如果不调用exec()函数,则不会产生任何事件循环。非常重要的一点是:QThread实例附属、依附于生成该QThread实例的线程,而不是附属、依附于通过调用run()函数生成的子线程。这意味着线程所有的队列信号槽函数和QMetaObject类的invokeMethod函数的执行都在创建QThread实例的线程中执行,而不是在调用run()函数生成的子线程中执行,因此,如果开发者想槽函数的执行在新生成的子线程中执行,必须用前文说到的工作对象(worker-object)的技术方法;新的槽函数不应该直接在QThread子类中实现。
不同于槽函数和QMetaObject类的invokeMethod函数的执行,那些在QThread对象上直接被调用的函数,将会在调用该函数的线程中执行。当子类化QThread时,始终要记住一点:当构造QThread时,构造函数(为便于后文描述,暂且称构造函数为construct() )将会在原来的线程(为便于后文描述,暂且称该线程为A)中执行,而一旦QThread对象构造完成且运行QThread对象的run()函数,则run函数是运行在新生成的子线程(为便于后文描述,暂且称该线程为B)而不是运行在A线程。如果成员变量从construct() 和run()这两个函数中都被访问,这意味着成员变量被两个不同线程A和B访问,请确保这样跨线程的访问的安全性,如:是否要线程同步、加线程锁、互斥访问等。
总结:
- 当在A线程中创建QThread对象,则被创建的QThread对象属于A线程,即依附、附属于A线程。
- 当在1中创建的QThread对象的run函数运行(通过调用QThread类的start()方法)时,会产生一个新的子线程B。A和B是属于不同的线程。
如下代码打印出了A、B线程的id,可以明显看出A、B属于不同线程:
myThread.cpp:
#include "myThread.h"
#include<QDebug>
CMyThread::CMyThread(QObject* parent/* = nullptr*/)
:QThread(parent)
{
qDebug() <<"A:" << this->currentThreadId();
}
CMyThread::~CMyThread()
{
}
void CMyThread::run()
{
qDebug() << "B:" << this->currentThreadId();
}
myThread.h:
#pragma once
#include <QThread>
class CMyThread :
public QThread
{
public:
CMyThread(QObject* parent = nullptr);
~CMyThread();
private:
virtual void run() override;
};
main.cpp:
#include <QtCore/QCoreApplication>
#include "myThread.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
CMyThread myThread;
myThread.start();
return a.exec();
}
运行结果如下:
4.QThread类常见踩雷、入坑、崩溃汇总说明
第3节部分提到了很多需要注意的点,尤其是标红的部分。如果对这些理解不透,往往会导致利用QThread类进行编程时,会出现各种崩溃,即使是从事Qt多年的程序猿也感觉到很难排查,非常棘手,这也是开篇我吐槽说在多线程编程中,尽量用STL的std::thread进行多线程编程而最好不用QThread类进行多线程编程。但既然是从事Qt的猿,本着钻研的科学精神,我们还是理解透为好。下面举例说明QThread类的坑:
myObject.h代码如下:
#pragma once
#include <QObject>
class CMyObject :
public QObject
{
Q_OBJECT
public:
CMyObject(QObject* parent = nullptr){};
~CMyObject(){};
};
myThread.h代码如下:
#pragma once
#include <QThread>
#include "myObject.h"
class CMyThread :
public QThread
{
public:
CMyThread(QObject* parent = nullptr);
~CMyThread();
public:
private:
virtual void run() override;
private:
CMyObject* m_pMyObj{nullptr};
};
myThread.cpp代码如下:
#include "myThread.h"
#include<QDebug>
#include <iostream>
#include <iomanip>
#include<QCoreApplication>
CMyThread::CMyThread(QObject* parent/* = nullptr*/)
:QThread(parent)
{
m_pMyObj = new CMyObject(this);
// 输出创建CMyThread对象的线程id,也就CMyThread对象依附、附属的线程id
qDebug() <<"AThreadId:" << this->currentThreadId();
// 输出主线程id
qDebug() << "MainThreadId:" << qApp->thread()->currentThreadId();
// 设置16进制输出
std::cout.setf(std::ios_base::hex, std::ios_base::basefield);
std::cout.setf(std::ios_base::showbase);
// 以16进制格式输出线程句柄
std::cout << "AThreadHandle:" << (qint64)this << "\r\n";
}
CMyThread::~CMyThread()
{
}
void CMyThread::run()
{
// 输出新生成的子线程id
qDebug() << "BThreadId:" << this->currentThreadId();
auto pObj = new QObject(m_pMyObj);
//m_pMyObj->setParent(pObj);
}
main.cpp代码如下:
#include <QtCore/QCoreApplication>
#include "myThread.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
CMyThread myThread;
myThread.start();
return a.exec();
}
运行上述代码,则出现如下警告:
上面是控制台程序,如果换成QWidget的程序,则会弹出如下中断框:
错误原因分析:
调用QThread类start()后,QThread类的run()就会开始运行,且run函数会产生一个新的子线程(暂且称为B线程),从上面打印的可以看到构造QThread类对象的线程id为:0x5434(暂且称为A线程),构造出的QThread类对象的句柄为:0xf47dcff908,而B线程id为: 0x63b0,可以看到A、B是两个不同的线程。上面打印出了主线程id,即main函数所在线程id,可以看到这里的A线程其实就是主线程。myThread.cpp中第33行以m_pMyObj为父构造pObj对象,根据前面的讲解,则pObj属于线程B,而m_pMyObj在是CMyThread类的构造函数中创建,则m_pMyObj属于线程A。同样地,如果将第34行注释取消,第34行也会崩溃。因为第33、34行违背了前面讲解的:“所有对象依附的线程必须和它们的父对象依附的线程是同一个线程”原则,所以会崩溃。所以在利用QThread进行多线程编程时,牢记2个原则:
- 不能在run函数中构造一个对象,其父是非run函数中构造出的对象。
- 不能在非run函数中构造一个对象,其父是run函数中构造出的对象。
5.线程管理
当QThread启动时,会通过发射如下信号通知调用方:
[signal] void QThread::started()
当QThread结束时,会通过发射如下信号通知调用方:
[signal] void QThread::finished()
可通过如下函数查询线程是否执行完成:
bool QThread::isFinished() const
可通过如下函数查询线程是否在运行:
bool QThread::isRunning() const
可以通过调用如下函数使线程停止:
void QThread::exit(int returnCode = 0)
[slot] void QThread::quit()
在某些情况下,可以通过terminate函数,强力退出、杀死线程,尽管如此,不建议利用terminate函数,使用terminate强力退出线程是危险的,提倡线程优雅退出。
slot] void QThread::terminate()
为了对Qt 4.8版本及以前版本的兼容,可以通过连接QThread类对象的finished信号到QObject::deleteLater()槽函数,从而实现当QThread类对象表示的线程执行完后,对其解构、删除。利用如下函数,实现对线程阻塞,直到其它线程执行完或者指定的超时时间到。
bool QThread::wait(unsigned long time = ULONG_MAX)
QThread类提供了一些静态的、平台无关的sleep函数:
[static] void QThread::sleep(unsigned long secs)
[static] void QThread::msleep(unsigned long msecs)
[static] void QThread::usleep(unsigned long usecs)
分别以秒、毫秒、微秒来睡眠、阻塞线程指定时间。
因为Qt是基于事件驱动的,所以大体来说, wait() 和 the sleep()是不需要用到的。考虑通过捕获finished()信号来替代wait()函数;用QTimer来替代sleep函数的调用。静态函数currentThreadId() 和 currentThread()返回当前执行线程的标识符,前者返回一个平台指定的线程id,后者返回指向QThread的指针。
为了给线程一个名称(线程名称在某些情况需要用到,如:在linux中通过ps -l命令获取线程信息时),可以在线程启动之前,通过setObjectName()函数给QThread类对象设置名称。如果没有为QThread类对象设置名称,则QThread类对象的名称默认为QThread类对象运行时的类名。