這個是本文章例項的原始碼地址:https://gitee.com/CogenCG/QThreadExample.git
子類化QThread來實現多執行緒, QThread只有run函式是在新執行緒裡的,其他所有函式都在QThread生成的執行緒裡。正確啟動執行緒的方法是呼叫QThread::start()來啟動,如果直接呼叫run成員函式,這個時候並不會有新的執行緒產生( 原因: 可以檢視往期《QThread原始碼淺析》文章,瞭解下run函式是怎麼被呼叫的)。
一、步驟
- 子類化 QThread;
- 重寫run,將耗時的事件放到此函式執行;
- 根據是否需要事件迴圈,若需要就在run函式中呼叫 QThread::exec() ,開啟執行緒的事件迴圈。事件迴圈的作用可以檢視往期《QThread原始碼淺析》文章中《QThread::run()原始碼》小節進行閱讀;
- 為子類定義訊號和槽,由於槽函式並不會在新開的執行緒執行,所以需要在建構函式中呼叫 moveToThread(this)。 注意:雖然呼叫moveToThread(this)可以改變物件的執行緒依附性關係,但是QThread的大多數成員方法是執行緒的控制介面,QThread類的設計本意是將執行緒的控制介面供給舊執行緒(建立QThread物件的執行緒)使用。所以不要使用moveToThread()將該介面移動到新建立的執行緒中,呼叫moveToThread(this)被視為不好的實現。
接下來會通過 使用執行緒來實現計時器,並實時在UI上顯示 的例項來說明不使用事件迴圈和使用事件迴圈的情況。(此例項使用QTimer會更方便,此處為了說明QThread的使用,故使用執行緒來實現)
二、不使用事件迴圈例項
InheritQThread.hpp
class InheritQThread:public QThread
{
Q_OBJECT
public:
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
}
void StopThread(){
QMutexLocker lock(&m_lock);
m_flag = false;
}
protected:
//執行緒執行函式
void run(){
qDebug()<<"child 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;
}
}
}
signals:
void ValueChanged(int i);
public:
bool m_flag;
QMutex m_lock;
};
mainwindow.hpp
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
WorkerTh = new InheritQThread(this);
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
}
~MainWindow(){
delete ui;
}
public slots:
void setValue(int i){
ui->lcdNumber->display(i);
}
private slots:
void on_startBt_clicked(){
WorkerTh->start();
}
void on_stopBt_clicked(){
WorkerTh->StopThread();
}
void on_checkBt_clicked(){
if(WorkerTh->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
}
private:
Ui::MainWindow *ui;
InheritQThread *WorkerTh;
};
在使用多執行緒的時候,如果出現共享資源使用,需要注意資源搶奪的問題,例如上述InheritQThread類中m_flag變數就是一個多執行緒同時使用的資源,上面例子使用 QMutexLocker+QMutex 的方式對臨界資源進行安全保護使用,其實際是使用了 RAII技術:(Resource Acquisition Is Initialization),也稱為“資源獲取就是初始化”,是C++語言的一種管理資源、避免洩漏的慣用法。C++標準保證任何情況下,已構造的物件最終會銷燬,即它的解構函式最終會被呼叫。簡單的說,RAII 的做法是使用一個物件,在其構造時獲取資源,在物件生命期控制對資源的訪問使之始終保持有效,最後在物件析構的時候釋放資源。具體 QMutexLocker+QMutex 互斥鎖的原理以及使用方法,在這裡就不展開說了,這個知識點網上有很多非常好的文章。
效果:
(1)在不點【start】按鍵的時候,點選【check thread state】按鈕檢查執行緒狀態,該執行緒是未開啟的。
(2)按下【start】後效果如下,並檢視終端訊息列印資訊:
只有呼叫了QThread::start()後,子執行緒才是真正的啟動,並且只有在run()函式才處於子執行緒內。
(3)我們再試一下點選【stop】按鈕,然後檢查執行緒的狀態:
點選【stop】按鈕使 m_flag = false, 此時run函式也就可以跳出死迴圈,並且停止了執行緒的運作,之後我們就不能再次使用該執行緒了,也許有的人說,我再一次start不就好了嗎?再一次start已經不是你剛才使用的執行緒了,這是start的是一個全新的執行緒。到此子類化 QThread ,不使用事件迴圈的執行緒使用就實現了,就這麼簡單。
三、使用事件迴圈例項
run函式中的 while 或者 for 迴圈執行完之後,如果還想讓執行緒保持運作,後期繼續使用,那應該怎麼做?
可以啟動子執行緒的事件迴圈,並且使用訊號槽的方式繼續使用子執行緒。注意:一定要使用訊號槽的方式,否則函式依舊是在建立QThread物件的執行緒執行。
- 在run函式中新增QThread::exec()來啟動事件迴圈。(注意: 在沒退出事件迴圈時,QThread::exec()後面的語句都無法被執行,退出後程式會繼續執行其後面的語句);
- 為QThread子類定義訊號和槽;
- 在QThread子類建構函式中呼叫 moveToThread(this)(注意: 可以實現建構函式在子執行緒內執行,但此方法不推薦,更好的方法會在後期的文章進行介紹)。
接著上述的例項,在InheritQThread類建構函式中新增並且呼叫moveToThread(this);在run函式中新增exec();並定義槽函式:
/**************在InheritQThread建構函式新增moveToThread(this)**********/
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
moveToThread(this);
}
/**************在InheritQThread::run函式新增exec()***************/
void run(){
qDebug()<<"child 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;
}
}
exec(); //開啟事件迴圈
}
/************在InheritQThread類中新增QdebugSlot()槽函式***************/
public slots:
void QdebugSlot(){
qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
}
在MainWindow類中新增QdebugSignal訊號;在建構函式中將QdebugSignal訊號與InheritQThread::QdebugSlot槽函式進行綁;新增一個傳送QdebugSignal訊號的按鈕:
/**********在MainWindow建構函式中繫結訊號槽******************/
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
ui->setupUi(this);
WorkerTh = new InheritQThread(this);
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot); //繫結訊號槽
}
/********MainWindow類中新增訊號QdebugSignal槽以及按鈕事件槽函式**********/
signals:
void QdebugSignal(); //新增QdebugSignal訊號
private slots:
//按鈕的事件槽函式
void on_SendQdebugSignalBt_clicked()
{
emit QdebugSignal();
}
實現事件迴圈的程式已修改完成,來看下效果:
(1)在執行的時候為什麼會出現以下警告?
QObject::moveToThread: Cannot move objects with a parent
我們看到MainWindow類中是這樣定義InheritQThread類物件的:WorkerTh = new InheritQThread(this)。如果需要使用moveToThread()來改變物件的依附性,其建立時不能夠帶有父類。將語句改為:WorkerTh = new InheritQThread()即可。
(2)修改完成後,點選【start】啟動執行緒,然後點選【stop】按鈕跳出run函式中的while迴圈,最後點選【check thread state】按鈕來檢查執行緒的狀態,會是什麼樣的情況呢?
由上圖可以看到,執行緒依舊處於執行狀態,這是因為run函式中呼叫了exec(),此時執行緒正處於事件迴圈中。
(3)接下來再點選【Send QdebugSignal】按鈕來傳送QdebugSignal訊號。
由終端的列印資訊得知,InheritQThread::QdebugSlot槽函式是在子執行緒中執行的。
四、子類化QThread執行緒的訊號與槽
從上圖可知,事件迴圈是一個無止盡迴圈,事件迴圈結束之前,exec()函式後的語句無法得到執行。只有槽函式所線上程開啟了事件迴圈,它才能在對應訊號發射後被呼叫。無論事件迴圈是否開啟,訊號傳送後會直接進入槽函式所依附的執行緒的事件佇列,然而,只有開啟了事件迴圈,對應的槽函式才會線上程中得到呼叫。下面通過幾種情況來驗證下:
(1)程式碼和《三、使用事件迴圈》小節的程式碼一樣,然後進行如下的操作:點選【start】按鈕->再點選【Send QdebugSignal】按鈕,這個時候槽函式會不會被執行呢?
這種情況無論點多少次傳送QdebugSignal訊號,InheritQThread::QdebugSlot槽函式都不會執行。因為當前執行緒還處於while迴圈當中,如果需要實現槽函式在當前執行緒中執行,那麼當前執行緒就應該處於事件迴圈的狀態,也就是正在執行exec()函式。所以如果需要InheritQThread::QdebugSlot槽函式執行,就需要點選【stop】按鈕退出while迴圈,讓執行緒進入事件迴圈。
(2)在《三、使用事件迴圈》小節的程式碼基礎上,把InheritQThread::run函式刪除,然後進行如下的操作:點選【start】啟動執行緒->點選【stop】按鈕跳出run函式中的while迴圈進入事件迴圈->點選【Send QdebugSignal】按鈕來傳送QdebugSignal訊號,會有什麼結果呢?
結果會和上面第一種情況一樣,雖然訊號已經在子執行緒的事件佇列上,但是由於子執行緒沒有事件迴圈,所以槽函式永遠都不會被執行。
(3)在上面《三、使用事件迴圈》小節的程式碼基礎上,將InheritQThread建構函式中的 moveToThread(this) 去除掉。進行如下操作:點選【start】啟動執行緒->點選【stop】按鈕跳出run函式中的while迴圈進入事件迴圈->點選【Send QdebugSignal】按鈕來傳送QdebugSignal訊號,會有什麼結果呢?
由上圖可以看出InheritQThread::QdebugSlot槽函式居然是在GUI主執行緒中執行了。因為InheritQThread物件我們是在主執行緒中new出來的,如果不使用moveToThread(this)來改變物件的依附性關係,那麼InheritQThread物件就是屬於GUI主執行緒,根據connect訊號槽的執行規則,最終槽函式會在物件所依賴的執行緒中執行。訊號與槽繫結的connect函式的細節會在後期的《跨執行緒的訊號槽》文章進行單獨介紹。
五、如何正確退出執行緒並釋放資源
InheritQThread類的程式碼不變動,和上述的程式碼一樣:
#ifndef INHERITQTHREAD_H
#define INHERITQTHREAD_H
#include <QThread>
#include <QMutex>
#include <QMutexLocker>
#include <QDebug>
class InheritQThread:public QThread
{
Q_OBJECT
public:
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
moveToThread(this);
}
void StopThread(){
QMutexLocker lock(&m_lock);
m_flag = false;
}
protected:
//執行緒執行函式
void run(){
qDebug()<<"child 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;
}
}
exec();
}
signals:
void ValueChanged(int i);
public slots:
void QdebugSlot(){
qDebug()<<"QdebugSlot function is in thread:"<<QThread::currentThreadId();
}
public:
bool m_flag;
QMutex m_lock;
};
#endif // INHERITQTHREAD_H
MainWindow類新增ExitBt、TerminateBt兩個按鈕,用於呼叫WorkerTh->exit(0)、WorkerTh->terminate()退出執行緒函式。由往期《QThread原始碼淺析》文章中《QThread::quit()、QThread::exit()、QThread::terminate()原始碼》小節得知呼叫quit和exit是一樣的,所以本處只新增了ExitBt按鈕:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQThread.h"
#include <QThread>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr) :
QMainWindow(parent),
ui(new Ui::MainWindow){
qDebug()<<"GUI thread = "<<QThread::currentThreadId();
ui->setupUi(this);
WorkerTh = new InheritQThread();
connect(WorkerTh, &InheritQThread::ValueChanged, this, &MainWindow::setValue);
connect(this, &MainWindow::QdebugSignal, WorkerTh, &InheritQThread::QdebugSlot);
}
~MainWindow(){
delete ui;
}
signals:
void QdebugSignal();
public slots:
void setValue(int i){
ui->lcdNumber->display(i);
}
private slots:
void on_startBt_clicked(){
WorkerTh->start();
}
void on_stopBt_clicked(){
WorkerTh->StopThread();
}
void on_checkBt_clicked(){
if(WorkerTh->isRunning()){
ui->label->setText("Running");
}else{
ui->label->setText("Finished");
}
}
void on_SendQdebugSignalBt_clicked(){
emit QdebugSignal();
}
void on_ExitBt_clicked(){
WorkerTh->exit(0);
}
void on_TerminateBt_clicked(){
WorkerTh->terminate();
}
private:
Ui::MainWindow *ui;
InheritQThread *WorkerTh;
};
#endif // MAINWINDOW_H
執行上述的例程,點選【start】啟動執行緒按鈕,然後直接點選【exit(0)】或者【terminate()】,這樣會直接退出執行緒嗎?
點選【exit(0)】按鈕(猛點)
點選【terminate()】按鈕(就點一點)
由上述情況我們可以看到上面例程的執行緒啟動之後,無論怎麼點選【start】按鈕,執行緒都不會退出,點選【terminate()】按鈕的時候就會立刻退出當前執行緒。由往期《QThread原始碼淺析》文章中《QThread::quit()、QThread::exit()、QThread::terminate()原始碼》小節可以得知,若使用QThread::quit()、QThread::exit()來退出執行緒,該執行緒就必須要在事件迴圈的狀態(也就是正在執行exec()),執行緒才會退出。而QThread::terminate()不管執行緒處於哪種狀態都會強制退出執行緒,但這個函式存在非常多不安定因素,不推薦使用。我們下面來看看如何正確退出執行緒。
(1)如何正確退出執行緒?
- 如果執行緒內沒有事件迴圈,那麼只需要用一個標誌變數來跳出run函式的while迴圈,這就可以正常退出執行緒了。
- 如果執行緒內有事件迴圈,那麼就需要呼叫QThread::quit()或者QThread::exit()來結束事件迴圈。像剛剛舉的例程,不僅有while迴圈,迴圈後面又有exec(),那麼這種情況就需要先讓執行緒跳出while迴圈,然後再呼叫QThread::quit()或者QThread::exit()來結束事件迴圈。如下:
注意:儘量不要使用QThread::terminate()來結束執行緒,這個函式存在非常多不安定因素。
(2)如何正確釋放執行緒資源?
退出執行緒不代表執行緒的資源就釋放了,退出執行緒只是把執行緒停止了而已,那麼QThread類或者QThread派生類的資源應該如何釋放呢?直接 delete QThread類或者派生類的指標嗎?當然不能這樣做,千萬別手動delete執行緒指標,手動delete會發生不可預料的意外。理論上所有QObject都不應該手動delete,如果沒有多執行緒,手動delete可能不會發生問題,但是多執行緒情況下delete非常容易出問題,那是因為有可能你要刪除的這個物件在Qt的事件迴圈裡還排隊,但你卻已經在外面刪除了它,這樣程式會發生崩潰。 執行緒資源釋放分為兩種情況,一種是在建立QThread派生類時,新增了父物件,例如在MainWindow類中WorkerTh = new InheritQThread(this)讓主窗體作為InheritQThread物件的父類;另一種是不設定任何父類,例如在MainWindow類中WorkerTh = new InheritQThread()。
- 1、建立QThread派生類,有設定父類的情況:
這種情況,QThread派生類的資源都讓父類接管了,當父物件被銷燬時,QThread派生類物件也會被父類delete掉,我們無需顯示delete銷燬資源。但是子執行緒還沒結束完,主執行緒就destroy掉了(WorkerTh的父類是主執行緒視窗,主執行緒視窗如果沒等子執行緒結束就destroy的話,會順手把WorkerTh也delete這時就會奔潰了)。 注意:這種情況不能使用moveToThread(this)改變物件的依附性。 因此我們應該把上面MainWindow類的建構函式改為如下:
~MainWindow(){
WorkerTh->StopThread();//先讓執行緒退出while迴圈
WorkerTh->exit();//退出執行緒事件迴圈
WorkerTh->wait();//掛起當前執行緒,等待WorkerTh子執行緒結束
delete ui;
}
- 2、建立QThread派生類,沒有設定父類的情況:
也就是沒有任何父類接管資源了,又不能直接delete QThread派生類物件的指標,但是QObject類中有 void QObject::deleteLater () [slot] 這個槽,這個槽非常有用,後面會經常用到它用於安全的執行緒資源銷燬。我們通過檢視往期《QThread原始碼淺析》文章中《QThreadPrivate::start()原始碼》小節可知執行緒結束之後會發出 QThread::finished() 的訊號,我們將這個訊號和 deleteLater 槽繫結,執行緒結束後呼叫deleteLater來銷燬分配的記憶體。
在MainWindow類建構函式中,新增以下程式碼:
connect(WorkerTh, &QThread::finished, WorkerTh, &QObject::deleteLater)
~MainWindow()解構函式可以把 wait()函式去掉了,因為該執行緒的資源已經不是讓主視窗來接管了。當我們啟動執行緒之後,然後退出主視窗或者直接點選【stop】+【exit()】按鈕的時候,會出現以下的警告:
QThread::wait: Thread tried to wait on itself
QThread: Destroyed while thread is still running
為了讓子執行緒能夠響應訊號並在子執行緒執行槽函式,我們在InheritQThread類建構函式中新增了 moveToThread(this) ,此方法是官方極其不推薦使用的方法。那麼現在我們就遇到了由於這個方法引發的問題,我們把moveToThread(this)刪除,程式就可以正常結束和釋放資源了。那如果要讓子執行緒能夠響應訊號並在子執行緒執行槽函式,這應該怎麼做?在下一期會介紹一個官方推薦的《子類化QObject+moveToThread》的方法。
六、小結
- QThread只有run函式是在新執行緒裡;
- 如果必須需要實現線上程內執行槽的情景,那就需要在QThread的派生類建構函式中呼叫moveToThread(this),並且在run函式內執行QThread::exec()開啟事件迴圈;(極其不推薦使用moveToThread(this),下一期會介紹一種安全可靠的方法)
- 若需要使用事件迴圈,需要在run函式中呼叫QThread::exec();
- 儘量不要使用terminate()來結束執行緒,可以使用bool標誌位退出或者線上程處於事件迴圈時呼叫QThread::quit、QThread::exit來退出執行緒;
- 善用QObject::deleteLater來進行記憶體管理;
- 在QThread執行start函式之後,run函式還未執行完畢,再次start,不會發生任何結果;
- 子類化QThread多執行緒的方法適用於後臺執行長時間的耗時操作、單任務執行的、無需線上程內執行槽的情景。
這個是本文章例項的原始碼地址:https://gitee.com/CogenCG/QThreadExample.git