目錄
- 一、前言
- 二、QThread原始碼淺析
- 2.1 QThread類的定義原始碼
- 2.2 QThread::start()原始碼
- 2.3 QThreadPrivate::start()原始碼
- 2.4 QThread::run()原始碼
- 2.5 QThread::quit()、QThread::exit()、QThread::terminate()原始碼
- 2.6 章節小結
- 三、四種Qt多執行緒的實作方法
- 3.1 子類化QThread
- 3.1.1 步驟
- 3.1.2 不使用事件回圈實體
- 3.1.3 使用事件回圈實體
- 3.1.4 子類化QThread執行緒的信號與槽
- 3.1.5 如何正確退出執行緒并釋放資源
- 3.1.6 小結
- 3.2 子類化QObject+moveToThread
- 3.2.1 步驟
- 3.2.2 實體
- 3.2.3 如何正確使用執行緒(信號槽)和創建執行緒資源
- 3.2.4 如何正確退出執行緒并釋放資源
- 3.2.5 小結
- 3.3 繼承QRunnable+QThreadPool
- 3.3.1 步驟
- 3.3.2 實體
- 3.3.3 啟動執行緒的方式
- 3.3.4 如何與外界通信
- 3.3.5 小結
- 3.4 QtConcurrent::run()+QThreadPool
- 3.1 子類化QThread
- 四、跨執行緒的信號槽
- 五、總結
一、前言
在我們開發Qt程式時,會經常用到多執行緒和信號槽的機制,將耗時的事務放到單獨的執行緒,將其與GUI執行緒獨立開,然后通過信號槽的機制來進行資料通信,避免GUI界面假死的情況,例如:使用QT實作檔案的傳送,并且GUI界面需要實時顯示發送的進度,這時就需要將耗時的檔案資料操作放到獨立的執行緒中,然后把已傳送的進度資料通過信號發送到GUI執行緒,GUI主執行緒接收到信號后通過槽函式來更新UI,這樣界面就不會出現假死的情況了,
多執行緒和信號槽機制都是QT的關鍵技術之一,理解清楚這兩個技術點的關系,會讓你在開發程序中少走些彎路,少踩一些坑,本文章會介紹多種Qt多執行緒的實作方法,但是主要還是介紹有關于 信號槽機制的多執行緒 實作方法,在學習QT多執行緒的"那些事"前,我們不妨先思考下以下的一些問題,然后再帶著問題繼續往下看,這樣可能會有更好的理解:
【1】如何正確使用QT的多執行緒?
【2】執行緒start后,哪里才是執行緒正在啟動的地方?
【3】如何正確結束子執行緒以及資源釋放?
【4】重復呼叫QThread::start、QThread::quit()或QThread::exit()、QThread::terminate函式會有什么影響?
【5】呼叫QThread::quit()或QThread::exit()、QThread::terminate函式會不會立刻停止執行緒?
【6】多執行緒之間是怎么進行通信的?
【7】如何在子執行緒中啟動信號與槽的機制?
【8】QT中多執行緒之間的信號和槽是如何發送或執行的?
【9】如何正確使用信號與槽機制?
接下來我會通過我以前踩過的坑和開發經驗,并且通過一些實體來總結一下QT多執行緒和QT信號槽機制的知識點,
這個是本文章實體的原始碼地址:https://gitee.com/CogenCG/QThreadExample.git
二、QThread原始碼淺析
本章會挑出QThread原始碼中部分重點代碼來說明QThread啟動到結束的程序是怎么調度的,其次因為到了Qt4.4版本,Qt的多執行緒就有所變化,所以本章會以Qt4.0.1和Qt5.6.2版本的原始碼來進行淺析,
2.1 QThread類的定義原始碼
Qt4.0.1版本原始碼:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
public:
...//省略
explicit QThread(QObject *parent = 0);
~QThread();
...//省略
void exit(int retcode = 0);
public slots:
void start(QThread::Priority = InheritPriority); //啟動執行緒函式
void terminate(); //強制退出執行緒函式
void quit(); //執行緒退出函式
...//省略
signals:
void started(); //執行緒啟動信號
void finished(); //執行緒結束信號
...//省略
protected:
virtual void run() = 0;
int exec();
...//省略
};
#else // QT_NO_THREAD
Qt5.6.2版本原始碼:
#ifndef QT_NO_THREAD
class Q_CORE_EXPORT QThread : public QObject
{
Q_OBJECT
public:
...//省略
explicit QThread(QObject *parent = Q_NULLPTR);
~QThread();
...//省略
void exit(int retcode = 0); //執行緒退出函式
...//省略
public Q_SLOTS:
void start(Priority = InheritPriority); //啟動執行緒函式
void terminate(); //強制退出執行緒函式
void quit(); //執行緒退出函式
...//省略
Q_SIGNALS:
void started(QPrivateSignal); //執行緒啟動信號
void finished(QPrivateSignal); //執行緒結束信號
protected:
virtual void run();
int exec();
...//省略
};
#else // QT_NO_THREAD
從以上兩個版本的代碼可以看出,這些函式在宣告上基本沒什么差異,但是仔細看,兩個版本的 run() 函式宣告的是不是不一樣?
- Qt4.0.1版本run() 函式是純虛函式,即此類為抽象類不可以創建實體,只可以創建指向該類的指標,也就是說如果你需要使用QThread來實作多執行緒,就必須實作QThread的派生類并且實作 run() 函式;
- Qt5.6.2版本的run() 函式是虛函式,繼承QThread類時,可以重新實作 run() 函式,也可以不實作,
注:我查看了多個Qt版本的原始碼,發現出現以上差異的版本是從Qt4.4開始的,從Qt4.4版本開始,QThread類就不再是抽象類了,
2.2 QThread::start()原始碼
再來看看QThread::start()原始碼,Qt4.0.1版本和Qt5.6.2版本此部分的原始碼大同小異,所以以Qt5.6.2版本的原始碼為主,如下:
void QThread::start(Priority priority)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (d->isInFinish) {
locker.unlock();
wait();
locker.relock();
}
if (d->running)
return;
... ... // 此部分是d指標配置
#ifndef Q_OS_WINRT
... ... // 此部分為注釋
d->handle = (Qt::HANDLE) _beginthreadex(NULL, d->stackSize, QThreadPrivate::start,
this, CREATE_SUSPENDED, &(d->id));
#else // !Q_OS_WINRT
d->handle = (Qt::HANDLE) CreateThread(NULL, d->stackSize, (LPTHREAD_START_ROUTINE)QThreadPrivate::start,
this, CREATE_SUSPENDED, reinterpret_cast<LPDWORD>(&d->id));
#endif // Q_OS_WINRT
if (!d->handle) {
qErrnoWarning(errno, "QThread::start: Failed to create thread");
d->running = false;
d->finished = true;
return;
}
int prio;
d->priority = priority;
switch (d->priority) {
... ... // 此部分為執行緒優先級配置
case InheritPriority:
default:
prio = GetThreadPriority(GetCurrentThread());
break;
}
if (!SetThreadPriority(d->handle, prio)) {
qErrnoWarning("QThread::start: Failed to set thread priority");
}
if (ResumeThread(d->handle) == (DWORD) -1) {
qErrnoWarning("QThread::start: Failed to resume new thread");
}
}
挑出里面的重點來說明:
(1)Q_D()宏定義
在看原始碼的時候,當時比較好奇start函式的第一條陳述句 Q_D()宏定義 是什么意思,所以就看了下原始碼,在此也順便講講,Q_D() 原始碼是一個宏定義,如下:
#define Q_D(Class) Class##Private * const d = d_func()
此處利用了預處理宏里的 ## 運算子:連接前后兩個符號,變成一個新的符號,將Q_D(QThread)展開后,變成:QThreadPrivate * const d = d_func(),
(2)_beginthreadex()函式
上面d->handle = (Qt::HANDLE) _beginthreadex ( NULL, d->stackSize, QThreadPrivate::start, this, CREATE_SUSPENDED, &( d->id ) ) 陳述句中的函式是創建執行緒的函式,其原型以及各引數的說明如下:
unsigned long _beginthreadex(
void *security, // 安全屬性,NULL為默認安全屬性
unsigned stack_size, // 指定執行緒堆疊的大小,如果為0,則執行緒堆疊大小和創建它的執行緒的相同,一般用0
unsigned ( __stdcall *start_address )( void * ),
// 指定執行緒函式的地址,也就是執行緒呼叫執行的函式地址(用函式名稱即可,函式名稱就表示地址)
void *arglist, // 傳遞給執行緒的引數的指標,可以通過傳入物件的指標,在執行緒函式中再轉化為對應類的指標
//如果傳入this,這個this表示呼叫QThread::start的物件地址,也就是QThread或者其派生類物件本身
unsigned initflag, // 執行緒初始狀態,0:立即運行;CREATE_SUSPEND:suspended(懸掛)
unsigned *thrdaddr // 用于記錄執行緒ID的地址
);
2.3 QThreadPrivate::start()原始碼
從QThread::start()原始碼可以知道,QThreadPrivate::start是重點,其實際就是呼叫了QThreadPrivate::start(this),這個 this 表示呼叫QThread::start的物件地址,也就是QThread或者其派生類物件本身,因為兩個Qt版本此部分的原始碼大同小異,所以本部分主要是以5.6.2版本的原始碼為主,其原始碼以及說明如下:
// 引數arg就是上面所說的this
unsigned int __stdcall QT_ENSURE_STACK_ALIGNED_FOR_SSE QThreadPrivate::start(void *arg)
{
QThread *thr = reinterpret_cast<QThread *>(arg);
QThreadData *data = QThreadData::get2(thr);
// 創建執行緒區域存盤變數,存放執行緒id
qt_create_tls();
TlsSetValue(qt_current_thread_data_tls_index, data);
data->threadId = reinterpret_cast<Qt::HANDLE>(quintptr(GetCurrentThreadId()));
QThread::setTerminationEnabled(false);
{
QMutexLocker locker(&thr->d_func()->mutex);
data->quitNow = thr->d_func()->exited;
}
if (data->eventDispatcher.load()) // custom event dispatcher set?
data->eventDispatcher.load()->startingUp();
else
createEventDispatcher(data);
...//省略
emit thr->started(QThread::QPrivateSignal()); // 發射執行緒啟動信號
QThread::setTerminationEnabled(true);
thr->run(); // 呼叫QThread::run()函式 -- 執行緒函式
finish(arg); //結束執行緒
return 0;
}
由上述原始碼可以看出,實際上 run() 函式是在這里呼叫的,并且發出了 started() 啟動信號,等到 run() 函式執行完畢,最后是呼叫了 QThreadPrivate::finish 函式結束執行緒,并且在finish內會發出 QThread::finished() 執行緒已結束的信號,
2.4 QThread::run()原始碼
再看看QThread::run()函式的原始碼,在上面 《2.1 QThread類的定義原始碼》的小節,我們可以看到兩個Qt版本宣告此方法的方式不一樣,Qt-4.0版本將此定義為了純虛函式,而Qt-5.6版本將此定義為了虛函式,那我們就看看Qt-5.6版本中,QThread::run()是如何定義的,如下:
void QThread::run()
{
(void) exec();
}
-
每一個 Qt 應用程式至少有一個 事件回圈 ,就是呼叫了 QCoreApplication::exec() 的那個事件回圈,不過,QThread也可以開啟事件回圈,只不過這是一個受限于執行緒內部的事件回圈,因此我們將處于呼叫main()函式的那個執行緒,并且由 QCoreApplication::exec() 創建開啟的那個事件回圈成為 主事件回圈 ,或者直接叫 主回圈 ,注意,QCoreApplication::exec()只能在呼叫main()函式的執行緒呼叫,主回圈所在的執行緒就是主執行緒,也被成為 GUI 執行緒,因為所有有關 GUI 的操作都必須在這個執行緒進行,QThread的區域事件回圈則可以通過在 QThread::run() 中呼叫 QThread::exec() 開啟,
-
我們通過以上原始碼可以看到,它的定義很簡單,就是呼叫了一個函式:QThread::exec() 開啟執行緒中的 事件回圈 ,我們也可以通過繼承QThread,重寫run()函式的方式,讓其實作相對復雜的邏輯代碼,如果你的執行緒需要將某些槽函式在本執行緒完成的話,就必須開啟事件回圈,否則在執行緒內無法回應各種信號并作出相應的行為,
小結: 比Qt-4.4版本更早的版本中,我們使用QThread啟動執行緒時,就必須要實作繼承于QThread的派生類,并且一定要重寫run函式,若需要使用事件回圈,就需要在run函式中添加exec(),到了Qt4.4版本之后(包括Qt4.4版本),QThread就不是抽象類了,不派生也可以實體化,在不重寫QThread::run()方法,start啟動執行緒是默認啟動事件回圈的,
注:當程式跑到了exec()代碼時,位于exec()后面的代碼就不會再被執行,除非我們使用quit、exit等退出陳述句來退出事件回圈,退出后,程式才會繼續執行位于exec()后面的代碼,
2.5 QThread::quit()、QThread::exit()、QThread::terminate()原始碼
執行緒停止函式的區別,從Qt原始碼來分析:
(1)QThread::quit()、QThread::exit()
//QThread::quit()宣告
void quit();
//QThread::quit()定義
void QThread::quit()
{ exit(); }
//QThread::exit()宣告
void exit(int retcode = 0);
//QThread::exit()定義
void QThread::exit(int returnCode)
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
d->exited = true;
d->returnCode = returnCode;
d->data->quitNow = true;
for (int i = 0; i < d->data->eventLoops.size(); ++i) {
QEventLoop *eventLoop = d->data->eventLoops.at(i);
eventLoop->exit(returnCode);
}
}
由以上原始碼可知,QThread::quit() 和 QThread::exit(0) 的呼叫是等效的,都是告訴執行緒的事件回圈,以回傳碼0(成功)退出,如果執行緒沒有事件,則此函式不執行任何操作,也就是無效的,當執行緒擁有事件回圈并且正處于 事件回圈(QThread::exec()) 的狀態時,呼叫 QThread::quit()或者QThread::exit() 執行緒就會馬上停止,否則不會立刻停止執行緒,直到執行緒處于事件回圈也就是正在執行 QThread::exec() 時,才會停止執行緒,
如果重復呼叫 QThread::quit()或者QThread::exit() 會有什么影響嗎?
重復呼叫 QThread::quit()或者QThread::exit() 也不會有什么影響,因為只有擁有事件回圈的執行緒,這兩個函式才會生效停止執行緒的功能,
(2)QThread::terminate()
void QThread::terminate()
{
Q_D(QThread);
QMutexLocker locker(&d->mutex);
if (!d->running)
return;
if (!d->terminationEnabled) {
d->terminatePending = true;
return;
}
// Calling ExitThread() in setTerminationEnabled is all we can do on WinRT
#ifndef Q_OS_WINRT
TerminateThread(d->handle, 0);
#endif
QThreadPrivate::finish(this, false); //結束執行緒函式
}
在這個函式定義的最后一個陳述句,是呼叫了 QThreadPrivate::finish(this, false); 函式,其函式作用是直接退出執行緒,無論執行緒是否開啟了事件回圈都會生效,會馬上終止一個執行緒,但這個函式存在非常不安定因素,不推薦使用,
如果重復呼叫 QThread::terminate() 會有什么影響嗎?
沒有影響,我們可以看到函式體里面的第三條陳述句,它首先會判斷執行緒是否還在運行中,如果不是,會直接退出函式,就不會繼續往下執行呼叫QThreadPrivate::finish(this, false); 函式了,
2.6 章節小結
相信看了以上的一些QThread原始碼,都大概知道了QThread類的本質以及QThread開啟到結束的程序,這里我再簡單總結下:
(1)QThread的本質:
- QThread 是用來管理執行緒的,它所依附的執行緒和它管理的執行緒并不是同一個東西;
- QThread 所依附的執行緒,就是執行 QThread t 或 QThread * t=new QThread 所在的執行緒;
- QThread 管理的執行緒,就是 run 啟動的執行緒,也就是次執行緒,
(2)在這里針對Qt4.4版本之后(包括Qt4.4版本)簡單匯總一下執行緒啟動到結束的程序:
- QThread物件或者QThread派生類物件顯式呼叫QThread類中的外部start()方法;
- QThread::start()方法再呼叫QThreadPrivate::start()方法;
- 在QThreadPrivate::start()方法內呼叫了QThread::run()虛函式,對使用者來說到了這里才是真正進入了一個新的執行緒里面,也就是說定義QThread物件或者QThread派生類物件的時候,還是在原來的執行緒里面,只有進入run函式才是進入了新的執行緒;
- 在QThreadPrivate::start()方法呼叫QThread::run()虛函式結束后,就會繼續呼叫QThreadPrivate::finish()函式來結束執行緒,并發出執行緒結束的信號finished(),
(3)QThread::quit()、QThread::exit()、QThread::terminate():
- 對執行緒重復使用這三個停止執行緒的函式,沒有任何影響;
- 盡量不要使用QThread::terminate()停止執行緒,此方式是強制退出執行緒,沒有安全保障,
- 呼叫QThread::quit()和QThread::exit()一樣,
(4)Qt各版本QThread類的變化:
- Qt4.4版本之前QThread類是屬于抽象類, Qt4.4版本之后(包括4.4版本)不是抽象類,
三、四種Qt多執行緒的實作方法
Qt的多執行緒實作方法主要有四種形式:子類化QThread、子類化QObject+moveToThread、繼承QRunnable+QThreadPool、QtConcurrent::run()+QThreadPool,本文章會注重介紹前兩種實作方法:子類化QThread、子類化QObject+moveToThread,也會簡單介紹后兩種的使用,
注:QtConcurrent、QRunnable以及QThreadPool的類,在Qt-4.4版本才開始有,
3.1 子類化QThread
子類化QThread來實作多執行緒, QThread只有run函式是在新執行緒里的,其他所有函式都在QThread生成的執行緒里,正確啟動執行緒的方法是呼叫QThread::start()來啟動,如果直接呼叫run成員函式,這個時候并不會有新的執行緒產生( 原因: 可以查看第一章,run函式是怎么被呼叫的)
3.1.1 步驟
- 子類化 QThread;
- 重寫run,將耗時的事件放到此函式執行;
- 根據是否需要事件回圈,若需要就在run函式中呼叫 QThread::exec() ,開啟執行緒的事件回圈,事件回圈的作用可以跳到《2.4 QThread::run()原始碼》小節進行閱讀;
- 為子類定義信號和槽,由于槽函式并不會在新開的執行緒運行,所以需要在建構式中呼叫 moveToThread(this), 注意:雖然呼叫moveToThread(this)可以改變物件的執行緒依附性關系,但是QThread的大多數成員方法是執行緒的控制介面,QThread類的設計本意是將執行緒的控制介面供給舊執行緒(創建QThread物件的執行緒)使用,所以不要使用moveToThread()將該介面移動到新創建的執行緒中,呼叫moveToThread(this)被視為不好的實作,
接下來會通過《使用執行緒來實作計時器,并實時在UI上顯示》的實體來說明不使用事件回圈和使用事件回圈的情況,(此實體使用QTimer會更方便,此處為了說明QThread的使用,故使用執行緒來實作)
3.1.2 不使用事件回圈實體
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 ,不使用事件回圈的執行緒使用就實作了,就這么簡單,
3.1.3 使用事件回圈實體
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槽函式是在子執行緒中執行的,
3.1.4 子類化QThread執行緒的信號與槽

從上圖可知,事件回圈是一個無止盡回圈,事件回圈結束之前,exec()函式后的陳述句無法得到執行,只有槽函式所在執行緒開啟了事件回圈,它才能在對應信號發射后被呼叫,無論事件回圈是否開啟,信號發送后會直接進入槽函式所依附的執行緒的事件佇列,然而,只有開啟了事件回圈,對應的槽函式才會在執行緒中得到呼叫,下面通過幾種情況來驗證下:
(1)代碼和《3.1.3 使用事件回圈》小節的代碼一樣,然后進行如下的操作:點擊【start】按鈕->再點擊【Send QdebugSignal】按鈕,這個時候槽函式會不會被執行呢?


這種情況無論點多少次發送QdebugSignal信號,InheritQThread::QdebugSlot槽函式都不會執行,因為當前執行緒還處于while回圈當中,如果需要實作槽函式在當前執行緒中執行,那么當前執行緒就應該處于事件回圈的狀態,也就是正在執行exec()函式,所以如果需要InheritQThread::QdebugSlot槽函式執行,就需要點擊【stop】按鈕退出while回圈,讓執行緒進入事件回圈,
(2)在《3.1.3 使用事件回圈》小節的代碼基礎上,把InheritQThread::run函式洗掉,然后進行如下的操作:點擊【start】啟動執行緒->點擊【stop】按鈕跳出run函式中的while回圈進入事件回圈->點擊【Send QdebugSignal】按鈕來發送QdebugSignal信號,會有什么結果呢?
結果會和上面第一種情況一樣,雖然信號已經在子執行緒的事件佇列上,但是由于子執行緒沒有事件回圈,所以槽函式永遠都不會被執行,
(3)在上面《3.1.3 使用事件回圈》小節的代碼基礎上,將InheritQThread建構式中的 moveToThread(this) 去除掉,進行如下操作:點擊【start】啟動執行緒->點擊【stop】按鈕跳出run函式中的while回圈進入事件回圈->點擊【Send QdebugSignal】按鈕來發送QdebugSignal信號,會有什么結果呢?

由上圖可以看出InheritQThread::QdebugSlot槽函式居然是在GUI主執行緒中執行了,因為InheritQThread物件我們是在主執行緒中new出來的,如果不使用moveToThread(this)來改變物件的依附性關系,那么InheritQThread物件就是屬于GUI主執行緒,根據connect信號槽的執行規則,最終槽函式會在物件所依賴的執行緒中執行,信號與槽系結的connect函式的細節會在后面的《跨執行緒的信號槽》章節進行單獨介紹,
3.1.5 如何正確退出執行緒并釋放資源
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()退出執行緒函式,由《2.5 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()】按鈕的時候就會立刻退出當前執行緒,由《2.5 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] 這個槽,這個槽非常有用,后面會經常用到它用于安全的執行緒資源銷毀,我們通過以上的《2.3 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》的方法,
3.1.6 小結
- 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多執行緒的方法適用于后臺執行長時間的耗時操作、單任務執行的、無需在執行緒內執行槽的情景,
3.2 子類化QObject+moveToThread
從QThread原始碼可知,在Qt4.4之前,run 是純虛函式,必須子類化QThread來實作run函式,而從Qt4.4開始,QThread不再支持抽象類,run 默認呼叫 QThread::exec() ,不需要子類化QThread,只需要子類化一個QObject,通過QObject::moveToThread將QObject派生類移動到執行緒中即可,這是官方推薦的方法,而且使用靈活、簡單、安全可靠,如果執行緒要用到事件回圈,使用繼承QObject的多執行緒方法無疑是一個更好的選擇,
這個小節主要是說一下,子類化QObject+moveToThread的多執行緒使用方法以及一些注意問題,其中有很多細節的問題其實和《3.1 子類化QThread》這個小節是一樣的,在這里就不再多說了,不明白的可以到上一節找找答案,
3.2.1 步驟
- 寫一個繼承QObject的類,將需要進行復雜耗時的邏輯封裝到槽函式中,作為執行緒的入口,入口可以有多個;
- 在舊執行緒創建QObject派生類物件和QThread物件,最好使用堆分配的方式創建(new),并且最好不要為此兩個物件設定父類,便于后期程式的資源管理;
- 把obj通過moveToThread方法轉移到新執行緒中,此時obj不能有任何的父類;
- 把執行緒的finished信號和obj物件、QThread物件的 QObject::deleteLater 槽連接,這個信號槽必須連接,否則會記憶體泄漏;如果QObject的派生類和QThread類指標是需要重復使用,那么就需要處理由物件被銷毀之前立即發出的 QObject::destroyed 信號,將兩個指標設定為nullptr,避免出現野指標;
- 將其他信號與QObject派生類槽連接,用于觸發執行緒執行槽函式里的任務;
- 初始化完后呼叫 QThread::start() 來啟動執行緒,默認開啟事件回圈;
- 在邏輯結束后,呼叫 QThread::quit 或者 QThread::exit 退出執行緒的事件回圈,
3.2.2 實體
寫一個繼承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的狀態,那么這種方式會出現這樣的情況嗎?我們直接運行上面的實體,然后過段時間檢查執行緒的狀態:

發現執行緒是一直處于運行狀態的,那接下來我們說一下應該怎么正確使用這種方式創建的執行緒并正確退出執行緒和釋放資源,
3.2.3 如何正確使用執行緒(信號槽)和創建執行緒資源
(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等資源同理處理就可以了,
3.2.4 如何正確退出執行緒并釋放資源
(1)如何正確退出執行緒?
正確退出執行緒的方式,其實和上面《3.1.5 如何正確退出執行緒并釋放資源》小節所講到的差不多,就是要使用 quit 和 exit 來退出執行緒,避免使用 terminate 來強制結束執行緒,有時候會出現例外的情況,例如以上的實體,啟動之后,直接點擊 【terminate】按鈕,界面就會出現卡死的現象,
(2)如何正確釋放執行緒資源?
在上面《3.1.5 如何正確退出執行緒并釋放資源》小節也有講到,千萬別手動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】按鈕,結果如下圖:

3.2.5 小結
- 這種QT多執行緒的方法,實作簡單、使用靈活,并且思路清晰,相對繼承于QThread類的方式更有可靠性,這種方法也是官方推薦的實作方法,如果執行緒要用到事件回圈,使用繼承QObject的多執行緒方法無疑是一個更好的選擇;
- 創建QObject派生類物件不能帶有父類;
- 呼叫QThread::start是默認啟動事件回圈;
- 必須需要使用信號槽的方式使用執行緒;
- 需要注意跨線資源的創建,例如QTimer、QUdpSocket等資源,如果需要在子執行緒中使用,必須要在子執行緒創建;
- 要善用QObject::deleteLater 和 QObject::destroyed來進行記憶體管理 ;
- 盡量避免使用terminate強制退出執行緒,若需要退出執行緒,可以使用quit或exit;
3.3 繼承QRunnable+QThreadPool
此方法個人感覺使用的相對較少,在這里只是簡單介紹下使用的方法,我們可以根據使用的場景來選擇方法,
此方法和QThread的區別:
- 與外界通信方式不同,由于QThread是繼承于QObject的,但QRunnable不是,所以在QThread執行緒中,可以直接將執行緒中執行的結果通過信號的方式發到主程式,而QRunnable執行緒不能用信號槽,只能通過別的方式,等下會介紹;
- 啟動執行緒方式不同,QThread執行緒可以直接呼叫start()函式啟動,而QRunnable執行緒需要借助QThreadPool進行啟動;
- 資源管理不同,QThread執行緒物件需要手動去管理洗掉和釋放,而QRunnable則會在QThreadPool呼叫完成后自動釋放,
接下來就來看看QRunnable的用法、使用場景以及注意事項;
3.3.1 步驟
要使用QRunnable創建執行緒,步驟如下:
- 繼承QRunnable,和QThread使用一樣, 首先需要將你的執行緒類繼承于QRunnable;
- 重寫run函式,還是和QThread一樣,需要重寫run函式;
- 使用QThreadPool啟動執行緒,
3.3.2 實體
繼承于QRunnable的類:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
explicit CusRunnable(){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThread::msleep(1000);
}
};
#endif // INHERITQRUNNABLE_H
主界面類:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "ui_mainwindow.h"
#include "InheritQRunnable.h"
#include <QThreadPool>
#include <QDebug>
namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0) :
QMainWindow(parent),
ui(new Ui::MainWindow){
ui->setupUi(this);
m_pRunnable = new CusRunnable();
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QThreadPool::globalInstance()->start(m_pRunnable);
}
~MainWindow(){
qDebug() << __FUNCTION__ ;
delete ui;
}
private:
Ui::MainWindow *ui;
CusRunnable * m_pRunnable = nullptr;
};
#endif // MAINWINDOW_H
直接運行以上實體,結果輸出如下:
MainWindow 0x377c
run 0x66ac
~CusRunnable
我們可以看到這里列印的執行緒ID是不同的,說明是在不同執行緒中執行,而執行緒執行完后就自動進入到解構式中, 不需要手動釋放,
3.3.3 啟動執行緒的方式
上面我們說到要啟動QRunnable執行緒,需要QThreadPool配合使用,而呼叫方式有兩種:全域執行緒池和非全域執行緒池,
(1)使用全域執行緒池啟動
QThreadPool::globalInstance()->start(m_pRunnable);
(2)使用非全域執行緒池啟動
該方式可以控制執行緒最大數量, 以及其他設定,比較靈活,具體參照幫助檔案,
QThreadPool threadpool;
threadpool.setMaxThreadCount(1);
threadpool.start(m_pRunnable);
3.3.4 如何與外界通信
前面我們提到,因為QRunnable沒有繼承于QObject,所以沒法使用信號槽與外界通信,那么,如果要在QRunnable執行緒中和外界通信怎么辦呢,通常有兩種做法:
- 使用多繼承,讓我們的自定義執行緒類同時繼承于QRunnable和QObject,這樣就可以使用信號和槽,但是多執行緒使用比較麻煩,特別是繼承于自定義的類時,容易出現介面混亂,所以在專案中盡量少用多繼承,
- 使用QMetaObject::invokeMethod,
接下來只介紹使用QMetaObject::invokeMethod來通信:
QMetaObject::invokeMethod 函式定義如下:
static bool QMetaObject::invokeMethod(
QObject *obj, const char *member,
Qt::ConnectionType,
QGenericReturnArgument ret,
QGenericArgument val0 = QGenericArgument(Q_NULLPTR),
QGenericArgument val1 = QGenericArgument(),
QGenericArgument val2 = QGenericArgument(),
QGenericArgument val3 = QGenericArgument(),
QGenericArgument val4 = QGenericArgument(),
QGenericArgument val5 = QGenericArgument(),
QGenericArgument val6 = QGenericArgument(),
QGenericArgument val7 = QGenericArgument(),
QGenericArgument val8 = QGenericArgument(),
QGenericArgument val9 = QGenericArgument());
該函式就是嘗試呼叫obj的member函式,可以是信號、槽或者Q_INVOKABLE宣告的函式(能夠被Qt元物件系統喚起),只需要將函式的名稱傳遞給此函式,呼叫成功回傳true,失敗回傳false,member函式呼叫的回傳值放在ret中,如果呼叫是異步的,則不能計算回傳值,你可以將最多10個引數(val0、val1、val2、val3、val4、val5、val6、val7、val8和val9)傳遞給member函式,必須使用Q_ARG()和Q_RETURN_ARG()宏封裝引數,Q_ARG()接受型別名 + 該型別的常量參考;Q_RETURN_ARG()接受一個型別名 + 一個非常量參考,
QMetaObject::invokeMethod可以是異步呼叫,也可以是同步呼叫,這取決與它的連接方式Qt::ConnectionType type:
- 如果型別是Qt::DirectConnection,則會立即呼叫該成員,同步呼叫,
- 如果型別是Qt::QueuedConnection,當應用程式進入主事件回圈時,將發送一個QEvent并呼叫該成員,異步呼叫,
- 如果型別是Qt::BlockingQueuedConnection,該方法將以與Qt::QueuedConnection相同的方式呼叫,不同的地方:當前執行緒將阻塞,直到事件被傳遞,使用此連接型別在同一執行緒中的物件之間通信將導致死鎖,
- 如果型別是Qt::AutoConnection,如果obj與呼叫者在同一執行緒,成員被同步呼叫;否則,它將異步呼叫該成員,
我們在主界面中定一個函式,用于更新界面內容:
Q_INVOKABLE void setText(QString msg){
ui->label->setText(msg);
}
繼承于QRunnable的執行緒類,修改完成如下:
#ifndef INHERITQRUNNABLE_H
#define INHERITQRUNNABLE_H
#include <QRunnable>
#include <QWidget>
#include <QDebug>
#include <QThread>
class CusRunnable : public QRunnable
{
public:
//修改建構式
explicit CusRunnable(QObject *obj):m_pObj(obj){
}
~CusRunnable(){
qDebug() << __FUNCTION__;
}
void run(){
qDebug() << __FUNCTION__ << QThread::currentThreadId();
QMetaObject::invokeMethod(m_pObj,"setText",Q_ARG(QString,"hello world!")); //此處與外部通信
QThread::msleep(1000);
}
private:
QObject * m_pObj = nullptr; //定義指標
};
#endif // INHERITQRUNNABLE_H
創建執行緒物件時,需要將主界面物件傳入執行緒類,如下:
m_pRunnable = new CusRunnable(this);
到這里也就實作了執行緒與外部通信了,運行效果如下:

3.3.5 小結
- 使用該方法實作的多執行緒,執行緒中的資源無需用戶手動釋放,執行緒執行完后會自動回收資源;
- 和繼承QThread的方法一樣需要繼承類,并且重新實作run函式;
- 需要結合QThreadPool執行緒池來使用;
- 與外界通信可以使用如果使用信號槽機制會比較麻煩,可以使用QMetaObject::invokeMethod的方式與外界通信,
3.4 QtConcurrent::run()+QThreadPool
在QT開發的場景中,個人覺得此方法使用的也比較少,所以本文只作一個簡單使用的介紹,QtConcurrent 是命名空間 (namespace),它提供了高層次的函式介面 (APIs),使所寫程式,可根據計算機的 CPU 核數,自動調整運行的執行緒數目,本文以 Qt 中的 QtConcurrent::run() 函式為例,介紹如何將函式運行在單獨的執行緒中,
(1)使用 QtConcurrent 模塊,需要在 .pro 中添加:
QT += concurrent
(2)將一個普通函式運行在單獨執行緒:
#include <QApplication>
#include <QDebug>
#include <QThread>
#include <QtConcurrent>
void fun1(){
qDebug()<<__FUNCTION__<<QThread::currentThread();
}
void fun2(QString str1, QString str2){
qDebug()<<__FUNCTION__<<str1+str2<<QThread::currentThread();
}
int fun3(int i, int j){
qDebug()<<__FUNCTION__<<QThread::currentThread();
return i+j;
}
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
qDebug()<<__FUNCTION__<<QThread::currentThread();
//無引數的普通函式
QFuture<void> fut1 = QtConcurrent::run(fun1);
//有引數的普通函式
QFuture<void> fut2 = QtConcurrent::run(fun2, QString("Thread"),QString(" 2"));
//獲取普通函式的回傳值
int i=1, j=2;
QFuture<int> fut3 = QtConcurrent::run(fun3, i, j);
qDebug()<<"ret:"<<fut3.result();
//以上的例子,如果要為其指定執行緒池,可以將執行緒池的指標作為第一個引數傳遞進去
QThreadPool pool;
QFuture<void> fut4 = QtConcurrent::run(&pool, fun1);
fut1.waitForFinished();
fut2.waitForFinished();
fut3.waitForFinished();
fut4.waitForFinished();
return a.exec();
}
輸出結果:
qMain QThread(0xf380590)
fun2 "Thread 2" QThread(0x1ca7c758, name = "Thread (pooled)")
fun1 QThread(0x1ca7c6d8, name = "Thread (pooled)")
fun3 QThread(0x1ca7c5b8, name = "Thread (pooled)")
ret: 3
fun1 QThread(0x1ca7c438, name = "Thread (pooled)")
(3)將類中的成員函式單獨運行在執行緒中:
將類中的成員函式運行在某一個執行緒中,可將指向該類實體的參考或指標作為 QtConcurrent::run 的第一個引數傳遞進去,常量成員函式一般傳遞常量參考 (const reference),而非常量成員函式一般傳遞指標 (pointer),
- 常量成員函式
在一個單獨的執行緒中,呼叫 QByteArray 的常量成員函式 split(),傳遞給 run() 函式的引數是 bytearray
//常量成員函式QByteArray::split()
QByteArray bytearray = "hello,world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
QList<QByteArray> result = future.result();
qDebug()<<"result:"<<result;
- 非常量成員函式
在一個單獨的執行緒中,呼叫 QImage 的非常量成員函式 invertPixels(),傳遞給 run() 函式的引數是 &image
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished(); // At this point, the pixels in 'image' have been inverted
四、跨執行緒的信號槽
執行緒的信號槽機制需要開啟執行緒的事件回圈機制,即呼叫QThread::exec()函式開啟執行緒的事件回圈,
Qt信號-槽連接函式原型如下:
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection )
Qt支持5種連接方式
- Qt::DirectConnection(直連方式)(信號與槽函式關系類似于函式呼叫,同步執行)
當信號發出后,相應的槽函式將立即被呼叫,emit陳述句后的代碼將在所有槽函式執行完畢后被執行,當信號發射時,槽函式將直接被呼叫,無論槽函式所屬物件在哪個執行緒,槽函式都在發射信號的執行緒內執行, - Qt::QueuedConnection(佇列方式)(此時信號被塞到事件佇列里,信號與槽函式關系類似于訊息通信,異步執行)
當信號發出后,排隊到信號佇列中,需等到接收物件所屬執行緒的事件回圈取得控制權時才取得該信號,呼叫相應的槽函式,emit陳述句后的代碼將在發出信號后立即被執行,無需等待槽函式執行完畢,當控制權回到接收者所依附執行緒的事件回圈時,槽函式被呼叫,槽函式在接收者所依附執行緒執行, - Qt::AutoConnection(自動方式)
Qt的默認連接方式,如果信號的發出和接收信號的物件同屬一個執行緒,那個作業方式與直連方式相同;否則作業方式與佇列方式相同,如果信號在接收者所依附的執行緒內發射,則等同于直接連接如果發射信號的執行緒和接受者所依附的執行緒不同,則等同于佇列連接 - Qt::BlockingQueuedConnection(信號和槽必須在不同的執行緒中,否則就產生死鎖)
槽函式的呼叫情形和Queued Connection相同,不同的是當前的執行緒會阻塞住,直到槽函式回傳, - Qt::UniqueConnection
與默認作業方式相同,只是不能重復連接相同的信號和槽,因為如果重復連接就會導致一個信號發出,對應槽函式就會執行多次,
如果沒有特殊的要求我們connect函式選擇默認的連接方式就好,也就是connect的第五個引數不填寫就ok,例如:
connect(m_obj, &QObject::destroyed, this, &MainWindow::SetPtrNullptr);
五、總結
本文章分析了部分QThread原始碼,講解了四種QT多執行緒的實作方法,以及多執行緒信號槽連接的知識點,接下來我再簡單對以上四種QT多執行緒的實作方法,總結一下哪種情況該使用哪種 Qt 執行緒技術:
| 需要執行緒的生命周期 | 開發場景 | 解決方案 |
|---|---|---|
| 單次呼叫 | 在其他的執行緒中運行一個方法,當方法運行結束后退出執行緒, | (1)撰寫一個函式,然后利用 QtConcurrent::run()運行它;(2)從QRunnable 派生一個類,并利用全域執行緒池QThreadPool::globalInstance()->start()來運行它,(3) 從QThread派生一個類, 多載QThread::run() 方法并使用QThread::start()來運行它, |
| 單次呼叫 | 一個耗時的操作必須放到另一個執行緒中運行,在這期間,狀態資訊必須發送到GUI執行緒中, | 使用 QThread,,多載run方法并根據情況發送信號,.使用queued信號/槽連接來連接信號與GUI執行緒的槽, |
| 常駐 | 有一物件位于另一個執行緒中,將讓其根據不同的請求執行不同的操作,這意味與作業者執行緒之間的通信是必須的, | 從QObject 派生一個類并實作必要的槽和信號,將物件移到一個具有事件回圈的執行緒中,并通過queued信號/槽連接與物件進行通信, |
當然QT還有其他實作多執行緒的方法,例如使用QtConcurrent::map()函式、QSocketNotifier,具體怎么使用,這里就不再過多介紹了,
這個是本文章實體的原始碼地址:https://gitee.com/CogenCG/QThreadExample.git
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/213204.html
標籤:其他
