QThread、moveToThread用法详述

目录

1.吐槽

2.预备知识

2.1.Object对象和线程的密切关系

2.2.改变QObject类对象的线程依附关系

3.QThread类

4.QThread类常见踩雷、入坑、崩溃汇总说明

5.线程管理


1.吐槽

        QThread类提供了一种平台无关的方法对线程进行管理。但对于QThread类的熟练使用,即使是从事Qt开发多年的程序猿们,往往也会踩雷、入坑。总之:QThread类不好用、如果对该类理解不透,很容易导致程序崩溃。本人强烈建议:利用C++进行线程开发,请用STL中的std::thread,std::thread进行线程开发非常简单,不易出错。关于std::thread的使用 ,请参考如下链接:

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对象作为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线程中进行。依然以地球人为例子,moveToThread就好像将某个地球人的国籍改变了,如:a人国籍本来为英国籍,通过调用moveToThread后,a人及其所有孩子的国籍变为其它国籍了。

     说明:如果QObject类对象有父,则它不能被移动,上例中对b调用如下代码,Qt会报错、终止程序,如下代码不会改变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 &parameter) {
          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访问,请确保这样跨线程访问的安全性,如:是否要线程同步、加线程锁、互斥访问等。

总结:

  1. 当在A线程中创建QThread对象,则被创建的QThread对象属于A线程,即依附、附属于A线程。
  2. 当在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函数中构造出的对象。

myThread.cpp代码中的run函数注释掉,将main.cpp中的main函数的代码改为如下:

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    CMyThread myThread;
    auto pParentObj = new QObject;
    auto pSubObj = new QObject(pParentObj);

    // 将pSubObj依附的线程从主线程移动到myThread线程
    pSubObj->moveToThread(&myThread);
    myThread.start();
 
    return a.exec();
}

通过moveToThread函数将pSubObj依附的线程从主线程移动到myThread线程,则程序报如下错误:

 错误的原因就是2.1节提到的:当QObject对象有父时,调用QObject对象的moveToThread函数会失败 。

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类对象运行时的类名。

  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值