【QT】子类化QObject+moveToThread实现多线程

时间:2022-07-25 15:03:20

往期链接:

从往期《QThread源码浅析》可知,在Qt4.4之前,run 是纯虚函数,必须子类化QThread来实现run函数。而从Qt4.4开始,QThread不再支持抽象类,run 默认调用 QThread::exec() ,不需要子类化QThread,只需要子类化一个QObject,通过QObject::moveToThread将QObject派生类移动到线程中即可。这是官方推荐的方法,而且使用灵活、简单、安全可靠。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择。

这一期主要是说一下,子类化QObject+moveToThread的多线程使用方法以及一些注意问题,其中有很多细节的问题其实和往期《子类化QThread实现多线程》文章是一样的,在这里就不再多说了,不明白的可以到往期《子类化QThread实现多线程》文章找找答案。

一、步骤

  • 写一个继承QObject的类,将需要进行复杂耗时的逻辑封装到槽函数中,作为线程的入口,入口可以有多个;
  • 在旧线程创建QObject派生类对象和QThread对象,最好使用堆分配的方式创建(new),并且最好不要为此两个对象设置父类,便于后期程序的资源管理;
  • 把obj通过moveToThread方法转移到新线程中,此时obj不能有任何的父类;
  • 把线程的finished信号和obj对象、QThread对象的 QObject::deleteLater 槽连接,这个信号槽必须连接,否则会内存泄漏;如果QObject的派生类和QThread类指针是需要重复使用,那么就需要处理由对象被销毁之前立即发出的 QObject::destroyed 信号,将两个指针设置为nullptr,避免出现野指针;
  • 将其他信号与QObject派生类槽连接,用于触发线程执行槽函数里的任务;
  • 初始化完后调用 QThread::start() 来启动线程,默认开启事件循环;
  • 在逻辑结束后,调用 QThread::quit 或者 QThread::exit 退出线程的事件循环。

二、实例

写一个继承QObject的类:InheritQObject,代码如下:

#ifndef INHERITQOBJECT_H
#define INHERITQOBJECT_H #include <QObject>
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug> class InheritQObject : public QObject
{
Q_OBJECT
public:
explicit InheritQObject(QObject *parent = 0) : QObject(parent){ } //用于退出线程循环计时的槽函数
void StopTimer(){
qDebug()<<"Exec StopTimer thread = "<<QThread::currentThreadId();
QMutexLocker lock(&m_lock);
m_flag = false;
} signals:
void ValueChanged(int i); public slots:
void QdebugSlot(){
qDebug()<<"Exec QdebugSlot thread = "<<QThread::currentThreadId();
} //计时槽函数
void TimerSlot(){
qDebug()<<"Exec TimerSlot thread = "<<QThread::currentThreadId(); int i=0;
m_flag = true; while(1)
{
++i; emit ValueChanged(i);
QThread::sleep(1); {
QMutexLocker lock(&m_lock);
if( !m_flag )
break;
}
}
} private:
bool m_flag;
QMutex m_lock;
}; #endif // INHERITQOBJECT_H

mainwindow主窗口类,代码如下:

#ifndef MAINWINDOW_H
#define MAINWINDOW_H #include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQObject.h"
#include <QThread> namespace Ui {
class MainWindow;
} class MainWindow : public QMainWindow
{
Q_OBJECT public:
explicit MainWindow(QWidget *parent = 0) :
QMainWindow(parent),
ui(new Ui::MainWindow){ qDebug()<<"GUI thread = "<<QThread::currentThreadId(); ui->setupUi(this); //创建QThread线程对象以及QObject派生类对象,注意:都不需要设置父类
m_th = new QThread();
m_obj = new InheritQObject(); //改变m_obj的线程依附关系
m_obj->moveToThread(m_th); //释放堆空间资源
connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
//设置野指针为nullptr
connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
//连接其他信号槽,用于触发线程执行槽函数里的任务
connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);
connect(m_obj, &InheritQObject::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, m_obj, &InheritQObject::QdebugSlot); //启动线程,线程默认开启事件循环,并且线程正处于事件循环状态
m_th->start();
} ~MainWindow(){
delete ui;
} signals:
void StartTimerSignal();
void QdebugSignal(); private slots:
//触发线程执行m_obj的计时槽函数
void on_startBt_clicked(){
emit StartTimerSignal();
} //退出计时槽函数
void on_stopBt_clicked(){
m_obj->StopTimer();
} //检测线程状态
void on_checkBt_clicked(){
if(m_th->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
} void on_SendQdebugSignalBt_clicked(){
emit QdebugSignal();
} //退出线程
void on_ExitBt_clicked(){
m_th->exit(0);
} //强制退出线程
void on_TerminateBt_clicked(){
m_th->terminate();
} //消除野指针
void SetPtrNullptr(QObject *sender){
if(qobject_cast<QObject*>(m_th) == sender){
m_th = nullptr;
qDebug("set m_th = nullptr");
} if(qobject_cast<QObject*>(m_obj) == sender){
m_obj = nullptr;
qDebug("set m_obj = nullptr");
}
} //响应m_obj发出的信号来改变时钟
void setValue(int i){
ui->lcdNumber->display(i);
} private:
Ui::MainWindow *ui;
QThread *m_th;
InheritQObject *m_obj;
}; #endif // MAINWINDOW_H

通过以上的实例可以看到,我们无需重写 QThread::run 函数,也无需显式调用 QThread::exec 来启动线程的事件循环了,通过QT源码可以知道,只要调用 QThread::start 它就会自动执行 QThread::exec 来启动线程的事件循环。

子类化QThread实现多线程的创建方法,如果run函数里面没有死循环也没有调用exec开启事件循环的话,就算调用了 QThread::start 启动线程,最终过一段时间,线程依旧是会退出,处于finished的状态。那么这种方式会出现这样的情况吗?我们直接运行上面的实例,然后过段时间检查线程的状态:

【QT】子类化QObject+moveToThread实现多线程

发现线程是一直处于运行状态的。那接下来我们说一下应该怎么正确使用这种方式创建的线程并正确退出线程和释放资源。

三、如何正确使用线程(信号槽)和创建线程资源

(1)如何正确使用线程?

如果需要让线程去执行一些行为,那就必须要正确使用信号槽的机制来触发槽函数,其他的方式调用槽函数都只是在旧线程中执行,无法达到预想效果。在多线程中信号槽的细节,会在后期的《跨线程的信号槽》文章来讲解,这里我们先简单说如何使用信号槽来触发线程执行任务先。

通过以上的实例得知,MainWindow 构造函数中使用了connect函数将 StartTimerSignal() 信号和 InheritQObject::TimerSlot() 槽进行了绑定,代码语句如下:

connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);

当点击【startTime】按钮发出 StartTimerSignal() 信号时,这个时候就会触发线程去执行 InheritQObject::TimerSlot() 槽函数进行计时。

【QT】子类化QObject+moveToThread实现多线程

由上面的打印信息得知,InheritQObject::TimerSlot() 槽函数的确是在一个新的线程中执行了。在上面继承QThread的多线程方法中也有说到,在这个时候去执行QThread::exit或者是QThread::quit是无效的,退出的信号会一直挂在消息队列里,只有点击了【stopTime】按钮让线程退出 while 循环,并且线程进入到事件循环 ( exec() ) 中,才会生效,并退出线程。

如果将【startTime】按钮不是发出 StartTimerSignal() 信号,而是直接执行InheritQObject::TimerSlot() 槽函数,会是怎么样的结果呢?代码修改如下:

//触发线程执行m_obj的计时槽函数
void on_startBt_clicked(){
m_obj->TimerSlot();
}

【QT】子类化QObject+moveToThread实现多线程

【QT】子类化QObject+moveToThread实现多线程

我们会发现界面已经卡死,InheritQObject::TimerSlot() 槽函数是在GUI主线程执行的,这就导致了GUI界面的事件循环无法执行,也就是界面无法被更新了,所以出现了卡死的现象。所以要使用信号槽的方式来触发线程工作才是有效的,不能够直接调用obj里面的成员函数。

(2)如何正确创建线程资源?

有一些资源我们可以直接在旧线程中创建(也就是不通过信号槽启动线程来创建资源),在新线程也可以直接使用,例如实例中的bool m_flag和QMutex m_lock变量都是在就线程中定义的,在新线程也可以使用。但是有一些资源,如果你需要在新线程中使用,那么就必须要在新线程创建,例如定时器、网络套接字等,下面以定时器作为例子,代码按照下面修改:

/**********在InheritQObject类中添加QTimer *m_timer成员变量*****/
QTimer *m_timer; /**********在InheritQObject构造函数创建QTimer实例*****/
m_timer = new QTimer(); /**********在InheritQObject::TimerSlot函数使用m_timer*****/
m_timer->start(1000);

运行点击【startTime】按钮的时候,会出现以下报错:

QObject::startTimer: Timers cannot be started from another thread

由此可知,QTimer是不可以跨线程使用的,所以将程序修改成如下,将QTimer的实例创建放到线程里面创建:

/*********在InheritQObject类中添加Init的槽函数,将需要初始化创建的资源放到此处********/
public slots:
void Init(){
m_timer = new QTimer();
} /********在MainWindow类中添加InitSiganl()信号,并绑定信号槽***********/
//添加信号
signals:
void InitSiganl(); //在MainWindow构造函数添加以下代码
connect(this, &MainWindow::InitSiganl, m_obj, &InheritQObject::Init); //连接信号槽
emit InitSiganl(); //发出信号,启动线程初始化QTimer资源

这样QTimer定时器就属于新线程,并且可以正常使用啦。网络套接字QUdpSocket、QTcpSocket等资源同理处理就可以了。

四、如何正确退出线程并释放资源

(1)如何正确退出线程?

正确退出线程的方式,其实和往期《子类化QThread实现多线程》中《如何正确退出线程并释放资源》小节所讲到的差不多,就是要使用 quit 和 exit 来退出线程,避免使用 terminate 来强制结束线程,有时候会出现异常的情况。例如以上的实例,启动之后,直接点击 【terminate】按钮,界面就会出现卡死的现象。

(2)如何正确释放线程资源?

在往期《子类化QThread实现多线程》中《如何正确退出线程并释放资源》小节也有讲到,千万别手动delete QThread类或者派生类的线程指针,手动delete会发生不可预料的意外。理论上所有QObject都不应该手动delete,如果没有多线程,手动delete可能不会发生问题,但是多线程情况下delete非常容易出问题,那是因为有可能你要删除的这个对象在Qt的事件循环里还排队,但你却已经在外面删除了它,这样程序会发生崩溃。所以需要 善用QObject::deleteLater 和 QObject::destroyed来进行内存管理。如上面实例使用到的代码:

//释放堆空间资源
connect(m_th, &QThread::finished, m_obj, &QObject::deleteLater);
connect(m_th, &QThread::finished, m_th, &QObject::deleteLater);
//设置野指针为nullptr
connect(m_th, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr); //消除野指针
void SetPtrNullptr(QObject *sender){
if(qobject_cast<QObject*>(m_th) == sender){
m_th = nullptr;
qDebug("set m_th = nullptr");
} if(qobject_cast<QObject*>(m_obj) == sender){
m_obj = nullptr;
qDebug("set m_obj = nullptr");
}
}

当我们调用线程的 quit 或者 exit 函数,并且线程到达了事件循环的状态,那么线程就会在结束并且发出 QThread::finished 的信号来触发 QObject::deleteLater 槽函数,QObject::deleteLater就会销毁系统为m_obj、m_th对象分配的资源。这个时候m_obj、m_th指针就属于野指针了,所以需要根据QObject类或者QObject派生类对象销毁时发出来的 QObject::destroyed 信号来设置m_obj、m_th指针为nullptr,避免野指针的存在。

运行上面的实例,然后点击【exit】按钮,结果如下图:

【QT】子类化QObject+moveToThread实现多线程

五、小结

  • 这种QT多线程的方法,实现简单、使用灵活,并且思路清晰,相对继承于QThread类的方式更有可靠性,这种方法也是官方推荐的实现方法。如果线程要用到事件循环,使用继承QObject的多线程方法无疑是一个更好的选择;
  • 创建QObject派生类对象不能带有父类;
  • 调用QThread::start是默认启动事件循环;
  • 必须需要使用信号槽的方式使用线程;
  • 需要注意跨线资源的创建,例如QTimer、QUdpSocket等资源,如果需要在子线程中使用,必须要在子线程创建;
  • 要善用QObject::deleteLater 和 QObject::destroyed来进行内存管理 ;
  • 尽量避免使用terminate强制退出线程,若需要退出线程,可以使用quit或exit;