周末天冷,索性把電腦抱到床上上網,這幾天看了 dbzhang800 博客關於 Qt 事件循環的幾篇 Blog,發現自己對 Qt 的事件循環有不少誤解。從來只看到現象,這次借 dbzhang800 的博客,就代碼論事,因此了解到一些 Qt 深層的實現,雖然是在 Qt 龐大的構架里只算的是冰山的一角,確讓人頗為收益。
從 dbzhang800 的博客中轉載兩篇關於事件循環的文章,放在一起,寫作備忘。
再次提到的一點是:事件循環和線程沒有必然關系。 QThread 的 run() 方法始終是在一個單獨線程執行的,但只有在 run() 方法中使用了 exec() 才真正開啟了一個單獨的事件循環。
順便一提,模態對話框和事件循環也沒有必然關系,可以轉讀這篇文章 QDialog 模態對話框與事件循環。
以下內容轉載自 dbzhang800 的博客:
「QThread 的使用方法」
原文地址:http://hi.baidu.com/cyclone/blog/item/5fac3bc7ab1b90d1d10060f2.html
「QThread 使用探討」
原文地址:http://hi.baidu.com/cyclone/blog/item/a33794ee00acba262cf53442.html
QThread 的使用方法
起源
昨天不小心看到Qt開發人員( Bradley T. Hughes)Blog中的一片文章 you are-doing-it-wrong 。 結果看得頭昏腦脹:好歹也自學了近1年的Qt,也一直很小心、很認真地閱讀Qt和manual和例子等資料,卻被突然告知,QThread的正確使用方法是一種自己從沒見過,而且Qt manual、example、書籍中都沒有提到過的一種方法。到底怎么了...
莫非manual、exmaple以及資料中的介紹都是錯的??
認真看看其他的人的評論,總算理清了一點頭緒。所有事情源於 QThread 的事件循環!
QThread 的兩種使用方法
1. 不使用事件循環。這是官方的 Manual 、example 以及相關書籍中都介紹的一種的方法。
a. 子類化 QThread
b. 重載 run 函數,run函數內有一個 while 或 for 的死循環
c. 設置一個標記為來控制死循環的退出。
2. 使用事件循環。(博客 you are-doing-it-wrong 批駁的就是這種情況下的 一種用法。)
a. 子類化 QThread,
b. 重載 run 使其調用 QThread::exec()
c. 並為該類定義信號和槽,這樣一來,由於槽函數並不會在新開的 thread 運行,很多人為了解決這個問題在構造函數中調用
moveToThread(this)
而爭論和不解正是這樣的一條語句造成的。
Bradley T. Hughes 給出說明是: QThread 應該被看做是操作系統線程的接口或控制點,而不應該包含需要在新線程中運行的代碼。需要運行的代碼應該放到一個QObject的子類中,然后將該子類的對象moveToThread到新線程中。
另外
在Qt4.3(包括)之前,run 是虛函數,必須子類化QThread來實現run函數。
而從Qt4.4開始,qthreads-no-longer-abstract ,run 默認調用 QThread::exec() 。這樣一來不需要子類化 QThread 了,只需要子類化一個 QObject 就夠了,這正是被 Bradley T. Hughes推薦的方法。
終於看懂了,但不管怎么說,都應該是 QThread 當初的設計導致的這種問題,而所有文檔和例子中都沒有提到該如何使用Qthread 進一步加劇了對QThread的這種誤用。
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時,隊列連接就受限制了。其實只要理解了前面這些,沒有事件循環的情況很容易就想通了。
參看
- http://labs.qt.nokia.com/blogs/2010/06/17/youre-doing-it-wrong/
- http://labs.qt.nokia.com/blogs/2010/06/17/youre-doing-it-wrong/
- http://labs.qt.nokia.com/blogs/2006/12/04/threading-without-the-headache/
- http://labs.qt.nokia.com/blogs/2007/07/05/qthreads-no-longer-abstract/
- http://gitorious.org/qthreadhowto/qthreadhowto/trees/master
- http://blog.exys.org/entries/2010/QThread_affinity.html
- http://thesmithfam.org/blog/2010/02/07/talking-to-qt-threads/
- http://doc.qt.nokia.com/4.7/threads-starting.html
- http://doc.qt.nokia.com/4.7/threads-qobject.html
- http://doc.qt.nokia.com/4.7/qthread.html
http://blog.csdn.net/lynfam/article/details/7081757
