往期鏈接:
- 《QThread原始碼淺析》
- 《子類化QThread實作多執行緒》
本文章實體的原始碼地址:https://gitee.com/CogenCG/QThreadExample.git
從往期《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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/208557.html
標籤:C++
上一篇:求教,多執行緒臟讀問題!
