往期鏈接:
從往期《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的狀態。那么這種方式會出現這樣的情況嗎?我們直接運行上面的實例,然后過段時間檢查線程的狀態:
發現線程是一直處於運行狀態的。那接下來我們說一下應該怎么正確使用這種方式創建的線程並正確退出線程和釋放資源。
三、如何正確使用線程(信號槽)和創建線程資源
(1)如何正確使用線程?
如果需要讓線程去執行一些行為,那就必須要正確使用信號槽的機制來觸發槽函數,其他的方式調用槽函數都只是在舊線程中執行,無法達到預想效果。在多線程中信號槽的細節,會在后期的《跨線程的信號槽》文章來講解,這里我們先簡單說如何使用信號槽來觸發線程執行任務先。
通過以上的實例得知,MainWindow 構造函數中使用了connect函數將 StartTimerSignal() 信號和 InheritQObject::TimerSlot() 槽進行了綁定,代碼語句如下:
connect(this, &MainWindow::StartTimerSignal, m_obj, &InheritQObject::TimerSlot);
當點擊【startTime】按鈕發出 StartTimerSignal() 信號時,這個時候就會觸發線程去執行 InheritQObject::TimerSlot() 槽函數進行計時。
由上面的打印信息得知,InheritQObject::TimerSlot() 槽函數的確是在一個新的線程中執行了。在上面繼承QThread的多線程方法中也有說到,在這個時候去執行QThread::exit或者是QThread::quit是無效的,退出的信號會一直掛在消息隊列里,只有點擊了【stopTime】按鈕讓線程退出 while 循環,並且線程進入到事件循環 ( exec() ) 中,才會生效,並退出線程。
如果將【startTime】按鈕不是發出 StartTimerSignal() 信號,而是直接執行InheritQObject::TimerSlot() 槽函數,會是怎么樣的結果呢?代碼修改如下:
//觸發線程執行m_obj的計時槽函數
void on_startBt_clicked(){
m_obj->TimerSlot();
}
我們會發現界面已經卡死,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多線程的方法,實現簡單、使用靈活,並且思路清晰,相對繼承於QThread類的方式更有可靠性,這種方法也是官方推薦的實現方法。如果線程要用到事件循環,使用繼承QObject的多線程方法無疑是一個更好的選擇;
- 創建QObject派生類對象不能帶有父類;
- 調用QThread::start是默認啟動事件循環;
- 必須需要使用信號槽的方式使用線程;
- 需要注意跨線資源的創建,例如QTimer、QUdpSocket等資源,如果需要在子線程中使用,必須要在子線程創建;
- 要善用QObject::deleteLater 和 QObject::destroyed來進行內存管理 ;
- 盡量避免使用terminate強制退出線程,若需要退出線程,可以使用quit或exit;
本文章實例的源碼地址:https://gitee.com/CogenCG/QThreadExample.git