1. 程式人生 > >【QT】子類化QThread實現多執行緒

【QT】子類化QThread實現多執行緒

- [《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