這個是本文章實例的源碼地址: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