【QT】子類化QThread實現多執行緒
阿新 • • 發佈:2020-11-09
- [《QThread原始碼淺析》](https://www.cnblogs.com/lcgbk/p/13940142.html)
***這個是本文章例項的原始碼地址: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
```cpp
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 = "<setupUi(this);
qDebug()<<"GUI thread = "<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】按鈕檢查執行緒狀態,該執行緒是未開啟的。**
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112427205-1805374044.png)
**(2)按下【start】後效果如下,並檢視終端訊息列印資訊:**
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112446742-53866391.png)
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112452122-284179721.png)
只有呼叫了QThread::start()後,子執行緒才是真正的啟動,並且只有在run()函式才處於子執行緒內。
**(3)我們再試一下點選【stop】按鈕,然後檢查執行緒的狀態:**
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112529946-369890700.png)
點選【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();並定義槽函式:
```cpp
/**************在InheritQThread建構函式新增moveToThread(this)**********/
InheritQThread(QObject *parent = Q_NULLPTR):QThread(parent){
moveToThread(this);
}
/**************在InheritQThread::run函式新增exec()***************/
void run(){
qDebug()<<"child thread = "< exit(0)、WorkerTh->terminate()退出執行緒函式。由往期《QThread原始碼淺析》文章中《QThread::quit()、QThread::exit()、QThread::terminate()原始碼》小節得知呼叫quit和exit是一樣的,所以本處只添加了ExitBt按鈕:
```cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include
#include "ui_mainwindow.h"
#include "InheritQThread.h"
#include
#include
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 = "<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)】按鈕(猛點)
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112738784-1050849222.png)
點選【terminate()】按鈕(就點一點)
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112807098-2035266926.png)
由上述情況我們可以看到上面例程的執行緒啟動之後,無論怎麼點選【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()來結束事件迴圈。如下:
![](https://img2020.cnblogs.com/blog/2085020/202011/2085020-20201107112937869-1625125915.png)
***注意:儘量不要使用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類的建構函式改為如下:
```cpp
~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類建構函式中,新增以下程式碼:
```cpp
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/QThreadExampl