QThread 使用探討

QThread 似乎是很難的一個東西,特別是信號和槽,有非常多的人(儘管使用者本人往往不知道)在用不恰當(甚至錯誤)的方式在使用 QThread,隨便用google一搜,就能搜出大量結果出來。無怪乎Qt的開發人員 Bradley T. Hughes 聲嘶力竭地喊you are-doing-it-wrong

和衆多用戶一樣,初次看到這個時,感到 Bradley T. Hughes有 些莫名奇妙,小題大作。儘管不舒服,當時還是整理過一篇博客QThread 的使用方法

時間過去3個月,儘管依然沒怎麼用thread;但今天csdn論壇中有人問到這個問題,想想還是盡我所能整理一下吧。提升自己,方便他人,何樂而不爲呢?

QThread東西還是比較多的,而且我對底層對象瞭解有限,僅就一點進行展開(或許是大家最關心的一點):QThread中的slots在那個線程中執行?

QThread::run

run 函數是做什麼用的?Manual中說的清楚:

  • run 對於線程的作用相當於main函數對於應用程序。它是線程的入口,run的開始和結束意味着線程的開始和結束。

原文如下(這段話我們稱爲定理一吧):

  • The run() implementation is for a thread what the main() entry point is for the application. All code executed in a call stack that starts in the run() function is executed by the new thread, and the thread finishes when the function returns.

這麼短的文字一眼就看完了,可是,這是什麼意思呢?又能說明什麼問題呢?看段簡單代碼:

class Thread:public QThread 

Q_OBJECT 
public: 
Thread(QObject* parent=0):QThread(parent){} 
public slots: 
void slot() { ... } 
signals: 
void sig(); 
protected: 
void run() { ...} 
}; 

int main(int argc, char** argv) 

... 
Thread thread; 
... 
}

對照前面的定理,run函數中的代碼時確定無疑要在次線程中運行的,那麼其他的呢?比如 slot 是在次線程還是主線程中運行?

你想說主線程,但又心有不甘,對麼?

QObject::connect

涉及信號槽,我們就躲不過 connect 函數,只是這個函數大家太熟悉。我不好意思再用一堆廢話來描述它,但不說又不行,那麼折中一下,只看它的最後一個參數吧(爲了簡單起見,只看它最常用的3個值)

下面的列表,我們暫稱爲定理二:

  • 自動連接(Auto Connection)
    • 這是默認設置
    • 如果信號在接收者所依附的線程內發射,則等同於直接連接
    • 如果發射信號的線程和接受者所依附的線程不同,則等同於隊列連接
    • 也就是這說,只存在下面兩種情況
  • 直接連接(Direct Connection)
    • 當信號發射時,槽函數將直接被調用。
    • 無論槽函數所屬對象在哪個線程,槽函數都在發射信號的線程內執行。
  • 隊列連接(Queued Connection)
    • 當控制權回到接受者所依附線程的事件循環時,槽函數被調用。
    • 槽函數在接收者所依附線程執行。

同前面一樣,這些文字大家都能看懂。但含義呢?

不妨繼續拿前面的例子來看,slot 函數是在主線程還是次線程中執行呢?

定理二強調兩個概念:發送信號的線程 和 接收者所依附的線程。而 slot 函數屬於我們在main中創建的對象 thread,即thread依附於主線程

  • 隊列連接告訴我們:槽函數在接受者所依附線程執行。即 slot 將在主線程執行
  • 直接連接告訴我們:槽函數在發送信號的線程執行。信號在那個線程發送呢??不定!
  • 自動連接告訴我們:二者不同,等同於隊列連接。即 slot 在主線程執行

太繞了?不是麼(要徹底理解這幾句話,你可能需要看Qt meta-object系統和Qt event系統)

怎麼辦呢?

如果上兩節看不懂,就記住下面的話吧(自己總結的,用詞上估計會不太準確)。

  • QThread 是用來管理線程的,它所依附的線程和它管理的線程並不是同一個東西
  • QThread 所依附的線程,就是執行 QThread t(0) 或 QThread * t=new QThread(0) 的線程。也就是咱們這兒的主線程
  • QThread 管理的線程,就是 run 啓動的線程。也就是次線程
  • 因爲QThread的對象依附在主線程中,所以他的slot函數會在主線程中執行,而不是次線程。除非:
    • QThread 對象依附到次線程中(通過movetoThread)
    • slot 和信號是直接連接,且信號在次線程中發射
  • 但上兩種解決方法都不好,因爲QThread不是這麼用的(Bradley T. Hughes)

好了,不再添加更多文字了,看代碼,估計咱們都會輕鬆點

主線程(信號)QThread(槽)

這是 Qt Manual 和 例子中普遍採用的方法。 但由於manual沒說槽函數是在主線程執行的,所以不少人都認爲它應該是在次線程執行了。

  • 定義一個 Dummy 類,用來發信號
  • 定義一個 Thread 類,用來接收信號
    • 重載 run 函數,目的是打印 threadid
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug> 
class Dummy:public QObject 

Q_OBJECT 
public: 
Dummy(){} 
public slots: 
void emitsig() 

emit sig(); 

signals: 
void sig(); 
}; 

class Thread:public QThread 

Q_OBJECT 
public: 
Thread(QObject* parent=0):QThread(parent) 

//moveToThread(this); 

public slots: 
void slot_main() 

qDebug()<<"from thread slot_main:" <<currentThreadId(); 

protected: 
void run() 

qDebug()<<"thread thread:"<<currentThreadId(); 
exec(); 

}; 

#include "main.moc" 

int main(int argc, char *argv[]) 

  QCoreApplication a(argc, argv); 
qDebug()<<"main thread:"<<QThread::currentThreadId(); 
Thread thread; 
Dummy dummy; 
QObject::connect(&dummy, SIGNAL(sig()), &thread, SLOT(slot_main())); 
thread.start(); 
dummy.emitsig(); 
return a.exec(); 
}

然後看到結果(具體值每次都變,但結論不變)

main thread: 0x1a40 from thread slot_main: 0x1a40 thread thread: 0x1a48

看到了吧,槽函數的線程和主線程是一樣的!

如果你看過Qt自帶的例子,你會發現 QThread 中 slot 和 run 函數共同操作的對象,都會用QMutex鎖住。爲什麼?

因爲slot和run處於不同線程,需要線程間的同步!

如果想讓槽函數slot在次線程運行(比如它執行耗時的操作,會讓主線程死掉),怎麼解決呢?

  • 注意:dummy信號是在主線程發射的, 接收者 thread 也在主線程中。
  • 參考我們前面的結論,很容易想到:
    • 將 thread 依附的線程改爲次線程不就行了?
    • 這也是代碼中註釋掉的 moveToThread(this)所做的,去掉註釋,你會發現slot在次線程中運行

main thread: 0x13c0 
thread thread: 0x1de0 
from thread slot_main: 0x1de0

這可以工作,但這是 Bradley T. Hughes 強烈批判的用法。推薦的方法後面會給出。

run中信號與QThread中槽
  • 定義一個 Dummy 類,在run中發射它的信號
    • 也可以在run中發射 Thread 類中的信號,而不是Dummy(效果完全一樣)
  • QThread 定義槽函數,重載run函數
/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug> 

class Dummy:public QObject 

Q_OBJECT 
public: 
Dummy(QObject* parent=0):QObject(parent){} 
public slots: 
void emitsig() 

emit sig(); 

signals: 
void sig(); 
}; 

class Thread:public QThread 

Q_OBJECT 
public: 
Thread(QObject* parent=0):QThread(parent) 

//moveToThread(this); 

public slots: 
void slot_thread() 

qDebug()<<"from thread slot_thread:" <<currentThreadId(); 

signals: 
void sig(); 
protected: 
void run() 

qDebug()<<"thread thread:"<<currentThreadId(); 
Dummy dummy; 
connect(&dummy, SIGNAL(sig()), this, SLOT(slot_thread())); 
dummy.emitsig(); 
exec(); 

}; 

#include "main.moc" 

int main(int argc, char *argv[]) 

QCoreApplication a(argc, argv); 
qDebug()<<"main thread:"<<QThread::currentThreadId(); 
Thread thread; 
thread.start(); 
return a.exec(); 
}

想看結果麼?

main thread: 0x15c0 
thread thread: 0x1750 
from thread slot_thread: 0x15c0
  • 其實沒懸念,肯定是主線程
    • thread 對象本身在主線程。所以它的槽也在要在主線程執行

如何解決呢?

  • (方法一)前面提了 moveToThread,這兒可以用,而且可以解決問題。當同樣,是被批判的對象。
  • (方法二)注意哦,這兒我們的信號時次線程發出的,對比connect連接方式,會發現:
    • 採用直接連接,槽函數將在次線程(信號發出的線程)執行
    • 這個方法不太好,因爲你需要處理slot和它的對象所在線程的同步。需要 QMutex 一類的東西
推薦的方法

千呼萬喚始出來。

其實,這個方法太簡單,太好用了。定義一個普通的QObject派生類,然後將其對象move到QThread中。使用信號和槽時根本不用考慮多線程的存在。也不用使用QMutex來進行同步,Qt的事件循環會自己自動處理好這個。

/*!
* \file main.cpp
*
* Copyright (C) 2010, dbzhang800
* All rights reserved.
*
*/

#include <QtCore/QCoreApplication> 
#include <QtCore/QObject> 
#include <QtCore/QThread> 
#include <QtCore/QDebug> 

class Dummy:public QObject 

Q_OBJECT 
public: 
Dummy(QObject* parent=0):QObject(parent) {} 
public slots: 
void emitsig() 

emit sig(); 

signals: 
void sig(); 
}; 

class Object:public QObject 

Q_OBJECT 
public: 
Object(){} 
public slots: 
void slot() 

qDebug()<<"from thread slot:" <<QThread::currentThreadId(); 

}; 

#include "main.moc" 

int main(int argc, char *argv[]) 

QCoreApplication a(argc, argv); 
qDebug()<<"main thread:"<<QThread::currentThreadId(); 
QThread thread; 
Object obj; 
Dummy dummy; 
obj.moveToThread(&thread); 
QObject::connect(&dummy, SIGNAL(sig()), &obj, SLOT(slot())); 
thread.start(); 
dummy.emitsig(); 
return a.exec(); 
}

結果:恩,slot確實不在主線程中運行(這麼簡單不值得歡呼麼?)

main thread: 0x1a5c 
from thread slot: 0x186c其他
  • 本文只考慮了使用事件循環的情況,也有可能run中沒有事件循環。這時信號與槽會與本文有點差別。比如run中使用connect時,隊列連接就受限制了。其實只要理解了前面這些,沒有事件循環的情況很容易就想通了。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章