You’re doing it wrong. — Bradley T. Hugheshtml
線程是qt channel裏最流行的討論話題之一。許多人加入了討論並詢問如何解決他們在運行跨線程編程時所遇到的問題。git
快速檢閱一下他們的代碼,在發現的問題當中,十之八九遇到得最大問題是他們在某個地方使用了線程,而隨後又墜入了並行編程的陷阱。Qt中建立、運行線程的「易用」性、缺少相關編程尤爲是異步網絡編程知識或是養成的使用其它工具集的習慣、這些因素和Qt的信號槽架構混合在一塊兒,便常常使得人們本身把本身射倒在了腳下。此外,Qt對線程的支持是把雙刃劍:它即便得你在進行Qt多線程編程時感受十分簡單,但同時你又必須對Qt所新添加許多的特性尤其當心,特別是與QObject的交互。web
本文的目的不是教你如何使用線程、如何適當地加鎖,也不是教你如何進行並行開發或是如何寫可擴展的程序;關於這些話題,有不少好書,好比這個連接給的推薦讀物清單. 這篇文章主要是爲了向讀者介紹Qt 4的事件循環以及線程使用,其目的在於幫助讀者們開發出擁有更好結構的、更加健壯的多線程代碼,並回避Qt事件循環以及線程使用的常見錯誤。編程
考慮到本文並非一個線程編程的泛泛介紹,咱們但願你有以下相關知識:設計模式
C++基礎; Qt 基礎:QOjbects , 信號/槽,事件處理; 瞭解什麼是線程、線程與進程間的關係和操做系統; 瞭解主流操做系統如何啓動、中止、等待並結束一個線程; 瞭解如何使用mutexes, semaphores 和以及wait conditions 來建立一個線程安全/可重入的函數、數據結構、類。 本文咱們將沿用以下的名詞解釋,即瀏覽器
可重入 一個類被稱爲是可重入的:只要在同一時刻至多隻有一個線程訪問同一個實例,那麼咱們說多個線程能夠安全地使用各自線程內本身的實例。 一個函數被稱爲是可重入的:若是每一次函數的調用只訪問其獨有的數據(譯者注:全局變量就不是獨有的,而是共享的),那麼咱們說多個線程能夠安全地調用這個函數。 也就是說,類和函數的使用者必須經過一些外部的加鎖機制來實現訪問對象實例或共享數據的序列化。 線程安全 若是多個線程能夠同時使用一個類的對象,那麼這個類被稱爲是線程安全的;若是多個線程能夠同時使用一個函數體裏的共享數據,那麼這個函數被稱爲線程安全的。 (譯者注: 更多可重入(reentrant)和t線程安全(thread-safe)的解釋: 對於類,若是它的全部成員函數均可以被不一樣線程同時調用而不相互影響——即便這些調用是針對同一個類對象,那麼該類被定義爲線程安全。 對於類,若是其不一樣實例能夠在不一樣線程中被同時使用而不相互影響,那麼該類被定義爲可重入。在Qt的定義中,在類這個層次,thread-safe是比reentrant更嚴格的要求)緩存
Qt做爲一個事件驅動的工具集,其事件和事件派發起到了核心的做用。本文將不會全面的討論這個話題,而是會聚焦於與線程相關的一些關鍵概念。想要了解更多的Qt事件系統專題參見 (這裏[doc.qt.nokia.com] 和 這裏 [doc.qt.nokia.com] ) (譯者注:也歡迎參閱譯者寫的博文:淺議Qt的事件處理機制一,二)安全
一個Qt的事件是表明了某件另人感興趣並已經發生的對象;事件與信號的主要區別在於,事件是針對於與咱們應用中一個具體目標對象(而這個對象決定了咱們如何處理這個事件),而信號發射則是「漫無目的」。從代碼的角度來講,全部的事件實例是QEvent [doc.qt.nokia.com]的子類,而且全部的QObject的派生類能夠重載虛函數QObject::event(),從而實現對目標對象實例事件的處理。服務器
事件能夠產生於應用程序的內部,也能夠來源於外部;好比:網絡
QKeyEvent和QMouseEvent對象表明了與鍵盤、鼠標相關的交互事件,它們來自於視窗管理程序。 當計時器開始計時,QTimerEvent 對象被髮送到QObject對象中,它們每每來自於操做系統。 當一個子類對象被添加或刪除時,QChildEvent對象會被髮送到一個QObject對象重,而它們來自於你的應用程序內部 對於事件來說,一個重要的事情在於它們並無在事件產生時被當即派發,而是列入到一個事件隊列(Event queue)中,等待之後的某一個時刻發送。分配器(dispatcher )會遍歷事件隊列,而且將入棧的事件發送到它們的目標對象當中,所以它們被稱爲事件循環(Event loop). 從概念上講,下段代碼描述了一個事件循環的輪廓:
1
2
3
4
5
6
7
|
1:
while
(is_active)
2: {
3:
while
(!event_queue_is_empty)
4: dispatch_next_event();
5:
6: wait_for_more_events();
7: }
|
咱們是經過運行QCoreApplication::exec()來進入Qt的主體事件循環的;這會引起阻塞,直至QCoreApplication::exit() 或者 QCoreApplication::quit() 被調用,進而結束循環。
這個「wait_for_more_events()」 函數產生阻塞,直至某個事件的產生。 若是咱們仔細想一想,會發現全部在那個時間點產生事件的實體一定是來自於外部的資源(由於當前全部內部事件派發已經結束,事件隊列裏也沒有懸而未決的事件等待處理),所以事件循環被這樣喚醒:
這也是*select(2)* 系統調用所作的: 它爲視窗管理活動監控了一組描述符,若是一段時間內沒有任何活動,它會超時。Qt所要作的是把系統調用select的返回值轉換爲正確的QEvent子類對象,並將其列入事件隊列的棧中,如今你知道事件循環裏面裝着什麼東西了吧:)
下面的清單並不全,但你會有一幅全景圖,你應該可以猜到哪些類須要使用事件循環。
在討論爲何*你永遠都不要阻塞事件循環*以前,讓咱們嘗試着再進一步弄明白到底「阻塞」意味着什麼。假定你有一個按鈕widget,它被按下時會emit一個信號;還有一個咱們下面定義的Worker對象鏈接了這個信號,並且這個對象的槽作了不少耗時的事情。當你點擊完這個按鈕後,從上之下的函數調用棧以下所示:
1
2
3
4
5
6
7
8
|
main(
int
,
char
**)
QApplication
::exec()
[...]
QWidget
::event(
QEvent
*)
Button::mousePressEvent(
QMouseEvent
*)
Button::clicked()
[...]
Worker::doWork()
|
在main()中,咱們經過調用QApplication::exec() (如上段代碼第2行所示)開啓了事件循環。視窗管理者發送了鼠標點擊事件,該事件被Qt內核捕獲,並轉換成QMouseEvent ,隨後經過QApplication::notify() (notify並無在上述代碼裏顯示)發送到咱們的widget的event()方法中(第4行)。由於Button並無重載event(),它的基類QWidget方法得以調用。 QWidget::event() 檢測出傳入的事件是一個鼠標點擊,並調用其專有的事件處理器,即Button::mousePressEvent() (第5行)。咱們重載了 mousePressEvent方法,併發射了Button::clicked()信號(第6行),該信號激活了咱們worker對象中十分耗時的Worker::doWork()槽(第8行)。(譯者注:若是你對這一段所描述得函數棧的更多細節,請參見淺議Qt的事件處理機制一,二)
當worker對象在繁忙的工做時,事件循環在作什麼呢? 你也許猜到了答案:什麼也沒作!它分發了鼠標點擊事件,而且因等待event handler返回而被阻塞。咱們阻塞了事件循環,也就是說,在咱們的doWork()槽(第8行)幹完活以前再不會有事件被派發了,也再不會有pending的事件被處理。
當事件派發被就此卡住時,widgets 也將不會再刷新本身(QPaintEvent對象將在事件隊列裏靜候),也不能有進一步地與widgets交互的事件發生,計時器也不會在開始計時,網絡通信也將變得遲鈍、停滯。更嚴重的是,許多視窗管理程序會檢測到你的應用再也不處理事件,從而告訴用戶你的程序再也不有響應(not responding). 這就是爲何快速的響應事件並儘量快的返回事件循環如此重要的緣由
那麼,對於須要長時間運行的任務,咱們應該怎麼作纔會不阻塞事件循環? 一個可行的答案是將這個任務移動另外一個線程中:在一節,咱們會看到若是去作。一個可能的方案是,在咱們的受阻塞的任務中,經過調用QCoreApplication::processEvents() 人工地強迫事件循環運行。QCoreApplication::processEvents() 將處理全部事件隊列中的事件並返回給調用者。
另外一個可選的強制地重入事件的方案是使用QEventLoop [doc.qt.nokia.com] 類,經過調用QEventLoop::exec() ,咱們重入了事件循環,並且咱們能夠把信號鏈接到QEventLoop::quit() 槽上使得事件循環退出,以下代碼所示:
1
2
3
4
5
6
|
1:
QNetworkAccessManager
qnam;
2:
QNetworkReply
*reply = qnam.get(
QNetworkRequest
(
QUrl
(...)));
3:
QEventLoop
loop;
4:
QObject
::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
5: loop.exec();
6:
/* reply has finished, use it */
|
QNetworkReply 沒有提供一個阻塞式的API,並且它要求運行一個事件循環。咱們進入到一個局部QEventLoop,而且當迴應完成時,局部的事件循環退出。
當重入事件循環是從「其餘路徑」完成的則要很是當心:它可能會致使無盡的遞歸循環!讓咱們回到Button這個例子。若是咱們再在doWork() 槽裏面調用QCoreApplication::processEvents() ,這時用戶又一次點擊了button,那麼doWork()槽將會再次被調用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
main(
int
,
char
**)
QApplication
::exec()
[...]
QWidget
::event(
QEvent
*)
Button::mousePressEvent(
QMouseEvent
*)
Button::clicked()
[...]
Worker::doWork()
// 實現,內部調用
QCoreApplication
::processEvents()
// 咱們人工的派發事件並且…
[...]
QWidget
::event(
QEvent
*)
// 另外一個鼠標點擊事件被髮送給Button
Button::mousePressEvent(
QMouseEvent
*)
Button::clicked()
// 這裏又一次emit了clicked() …
[...]
Worker::doWork()
// 完蛋! 咱們已經遞歸地調用了doWork槽
|
一個快速而且簡單的臨時解決辦法是把QEventLoop::ExcludeUserInputEvents 傳遞給QCoreApplication::processEvents(), 也就是說,告訴事件循環不要派發任何用戶輸入事件(事件將簡單的呆在隊列中)。
一樣地,使用一個對象的deleteLater() 來實現異步的刪除事件(或者,可能引起某種「關閉(shutdown)」的任何事件)則要警戒事件循環的影響。 (譯者注:deleteLater()將在事件循環中刪除對象並返回)
1
2
3
4
5
|
1:
QObject
*object =
new
QObject
;
2: object->deleteLater();
3:
QEventLoop
loop;
4: loop.exec();
5:
/* 如今object是一個野指針! */
|
能夠看到,咱們並無用QCoreApplication::processEvents() (從Qt 4.3以後,刪除事件再也不被派發 ),可是咱們確實用到了其餘的局部事件循環(像咱們QEventLoop 啓動的這個循環,或者下面將要介紹的QDialog::exec())。
切記當咱們調用QDialog::exec()或者 QMenu::exec()時,Qt進入了一個局部事件循環。Qt 4.5 之後的版本,QDialog 提供了QDialog::open() 方法用來再不進入局部循環的前提下顯示window-modal式的對話框
1
2
3
4
5
|
1:
QObject
*object =
new
QObject
;
2: object->deleteLater();
3:
QDialog
dialog;
4: dialog.exec();
5:
/* 如今object是一個野指針! */
|
Qt對線程的支持已經有不少年了(發佈於2000年九月22日的Qt2.2引入了QThread類),Qt 4.0版本的release則對其全部所支持平臺默認地是對多線程支持的。(固然你也能夠關掉對線程的支持,參見這裏)。如今Qt提供了很多類用於處理線程,讓你咱們首先預覽一下:
QThread 是Qt中一個對線程支持的核心的底層類。 每一個線程對象表明了一個運行的線程。因爲Qt的跨平臺特性,QThread成功隱藏了全部在不一樣操做系統裏使用線程的平臺相關性代碼。
爲了運用QThread從而讓代碼在一個線程裏運行,咱們能夠建立一個QThread的子類,並重載QThread::run() 方法:
1
2
3
4
5
6
|
class
Thread :
public
QThread
{
protected
:
void
run() {
/* your thread implementation goes here */
}
};
|
接着,咱們可使用:
class Thread : public QThread { protected: void run() { /* your thread implementation goes here */ } };
來真正的啓動一個新的線程。 請注意,Qt 4.4版本以後,QThread再也不支持抽象類;如今虛函數QThread::run()其實是簡單調用了QThread::exec(),而它啓動了線程的事件循環。(更多信息見後文)
QRunnable [doc.qt.nokia.com] 是一種輕量級的、以「run and forget」方式來在另外一個線程開啓任務的抽象類,爲了實現這一功能,咱們所須要作的所有事情是派生QRunnable 類,並實現純虛函數方法run()
class Task : public QRunnable { public: void run() { /* your runnable implementation goes here */ } };
事實上,咱們是使用QThreadPool 類來運行一個QRunnable 對象,它維護了一個線程池。經過調用QThreadPool::start(runnable) ,咱們把一個QRunnable 放入了QThreadPool的運行隊列中;只要線程是可見得,QRunnable 將會被拾起而且在那個線程裏運行。儘管全部的Qt應用程序都有一個全局的線程池,且它是經過調用QThreadPool::globalInstance()可見得,但咱們老是顯式地建立並管理一個私有的QThreadPool 實例。
請注意,QRunnable 並非一個QObject類,它並無一個內置的與其餘組件顯式通信的方法。你必須使用底層的線程原語(好比收集結構的枷鎖保護隊列等)來親自編寫代碼。
QtConcurrent 是一個構建在QThreadPool之上的上層API,它用於處理最普通的並行計算模式:map [en.wikipedia.org], reduce [en.wikipedia.org], and filter [en.wikipedia.org] 。同時,QtConcurrent::run()方法提供了一種便於在另外一個線程運行一個函數的方法。
不像QThread 以及QRunnable,QtConcurrent 沒有要求咱們使用底層的同步原語,QtConcurrent 全部的方法會返回一個QFuture 對象,它包含告終果並且能夠用來查詢線程計算的狀態(它的進度),從而暫停、繼續、取消計算。QFutureWatcher 能夠用來監聽一個QFuture 進度,而且經過信號和槽與之交互(注意QFuture是一個基於數值的類,它並無繼承自QObject).
\ | QThread | QRunnable | QtConcurrent1 |
---|---|---|---|
High level API | ✘ | ✘ | ✔ |
Job-oriented | ✘ | ✔ | ✔ |
Builtin support for pause/resume/cancel | ✘ | ✘ | ✔ |
Can run at a different priority | ✔ | ✘ | ✘ |
Can run an event loop | ✔ | ✘ | ✘ |
咱們在上文中已經討論了事件循環,咱們可能理所固然地認爲在Qt的應用程序中只有一個事件循環,但事實並非這樣:QThread對象在它們所表明的線程中開啓了新的事件循環。所以,咱們說main 事件循環是由調用main()的線程經過QCoreApplication::exec() 建立的。 它也被稱作是GUI線程,由於它是界面相關操做惟一容許的進程。一個QThread的局部事件循環能夠經過調用QThread::exec() 來開啓(它包含在run()方法的內部)
1
2
3
4
5
6
7
|
class
Thread :
public
QThread
{
protected
:
void
run() {
/* ... initialize ... */
exec();
}
};
|
正如咱們以前所提到的,自從Qt 4.4 的QThread::run() 方法再也不是一個純虛函數,它調用了QThread::exec()。就像QCoreApplication,QThread 也有QThread::quit() 和QThread::exit()來中止事件循環。
一個線程的事件循環爲駐足在該線程中的全部QObjects派發了全部事件,其中包括在這個線程中建立的全部對象,或是移植到這個線程中的對象。咱們說一個QObject的線程依附性(thread affinity)是指某一個線程,該對象駐足在該線程內。咱們在任什麼時候間均可以經過調用QObject::thread()來查詢線程依附性,它適用於在QThread對象構造函數中構建的對象。
1
2
3
4
5
6
7
8
9
10
11
12
|
class
MyThread :
public
QThread
{
public
:
MyThread()
{
otherObj =
new
QObject
;
}
private
:
QObject
obj;
QObject
*otherObj;
QScopedPointer
<
QObject
> yetAnotherObj;
};
|
如上述代碼,咱們在建立了MyThread 對象後,obj, otherObj, yetAnotherObj 的線程依附性是怎麼樣的?要回答這個問題,咱們必需要看一下建立他們的線程:是這個運行MyThread 構造函數的線程建立了他們。所以,這三個對象並無駐足在MyThread 線程,而是駐足在建立MyThread 實例的線程中。
要注意的是在QCoreApplication 對象以前建立的QObjects沒有依附於某一個線程。所以,沒有人會爲它們作事件派發處理。(換句話說,QCoreApplication 構建了表明主線程的QThread 對象)
咱們可使用線程安全的QCoreApplication::postEvent() 方法來爲某個對象分發事件。它將把事件加入到對象所駐足的線程事件隊列中。所以,除非事件對象依附的線程有一個正在運行的事件循環,不然事件不會被派發。
理解QObject和它全部的子類不是線程安全的(儘管是可重入的)很是重要;所以,除非你序列化對象內部數據全部可訪問的接口、數據,不然你不能讓多個線程同一時刻訪問相同的QObject(好比,用一個鎖來保護)。請注意,儘管你能夠從另外一個線程訪問對象,可是該對象此時可能正在處理它所駐足的線程事件循環派發給它的事件! 基於這種緣由,你不能從另外一個線程去刪除一個QObject,必定要使用QObject::deleteLater(),它會Post一個事件,目標刪除對象最終會在它所生存的線程中被刪除。(譯者注:QObject::deleteLater做用是,當控制流回到該對象所依附的線程事件循環時,該對象纔會被「本」線程中刪除)。
此外,QWidget 和它全部的子類,以及全部與GUI相關的類(即使不是基於QObject的,像QPixmap)並非可重入的。它們必須專屬於GUI線程。
咱們能夠經過調用QObject::moveToThread()來改變一個QObject的依附性;它將改變這個對象以及它的孩子們的依附性。由於QObject不是線程安全的,咱們必須在對象所駐足的線程中使用此函數;也就是說,你只能將對象從它所駐足的線程中推送到其餘線程中,而不能從其餘線程中拉回來。此外,Qt要求一個QObject的孩子必須與它們的雙親駐足在同一個線程中。這意味着:
你不能使用QObject::moveToThread()做用於有雙親的對象; 你千萬不要在一個線程中建立對象的同時把QThread對象本身做爲它的雙親。 (譯者注:二者不在同一個線程中):
1
2
3
4
5
|
class
Thread :
public
QThread
{
void
run() {
QObject
obj =
new
QObject
(
this
);
// WRONG!!!
}
};
|
這是由於,QThread 對象駐足在另外一個線程中,即QThread 對象它本身被建立的那個線程中。
Qt一樣要求全部的對象應該在表明該線程的QThread對象銷燬以前得以刪除;實現這一點並不難:只要咱們全部的對象是在QThread::run() 方法中建立便可。(譯者注:run函數的局部變量,函數返回時得以銷燬)。
接着上面討論的,咱們如何應用駐足在其餘線程裏的QObject方法呢?Qt提供了一種很是友好並且乾淨的解決方案:向事件隊列post一個事件,事件的處理將以調用咱們所感興趣的方法爲主(固然這須要線程有一個正在運行的事件循環)。而觸發機制的實現是由moc提供的內省方法實現的(譯者注:有關內省的討論請參見個人另外一篇文章Qt的內省機制剖析):所以,只有信號、槽以及被標記成Q_INVOKABLE的方法纔可以被其它線程所觸發調用。
靜態方法QMetaObject::invokeMethod() 爲咱們作了以下工做:
1
2
3
4
|
QMetaObject
::invokeMethod(object,
"methodName"
,
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
|
請注意,由於上面所示的參數須要被在構建事件時進行硬拷貝,參數的自定義型別所對應的類須要提供一個共有的構造函數、析構函數以及拷貝構造函數。並且必須使用註冊Qt型別系統所提供的qRegisterMetaType() 方法來註冊這一自定義型別。
跨線程的信號槽的工做方式相相似。當咱們把信號鏈接到一個槽的時候,QObject::connect的第五個可選輸入參數用來特化這一鏈接類型:
請注意,在上述四種鏈接方式當中,發送對象駐足於哪個線程並不重要!對於automatic connection,Qt會檢查觸發信號的線程,而且與接收者所駐足的線程相比較從而決定到底使用哪種鏈接類型。特別要指出的是:當前的Qt文檔的聲明(4.7.1) 是錯誤的:
若是發射者和接受者在同一線程,其行爲與Direct Connection相同;,若是發射者和接受者不在同一線程,其行爲Queued Connection相同
由於,發送者對象的線程依附性在這裏可有可無。舉例子說明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
view plaincopy to clipboardprint?
class
Thread :
public
QThread
{
Q_OBJECT
signals:
void
aSignal();
protected
:
void
run() {
emit aSignal();
}
};
/* ... */
Thread
thread
;
Object obj;
QObject
::connect(&
thread
, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread
.start();
|
如上述代碼,信號aSignal() 將在一個新的線程裏被髮射(由線程對象所表明);由於它並非Object 對象駐足的線程,因此儘管Thread對象thread與Object對象obj在同一個線程,但仍然是queued connection被使用。
(譯者注:這裏做者分析的很透徹,但願讀者仔細揣摩Qt文檔的這個錯誤。 也就是說 發送者對象自己在哪個線程對與信號槽鏈接類型不起任何做用,起到決定做用的是接收者對象所駐足的線程以及發射信號(該信號與接受者鏈接)的線程是否是在同一個線程,本例中aSignal()在新的線程中被髮射,因此採用queued connection)。
另一個常見的錯誤以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
view plaincopy to clipboardprint?
class
Thread :
public
QThread
{
Q_OBJECT
slots:
void
aSlot() {
/* ... */
}
protected
:
void
run() {
/* ... */
}
};
/* ... */
Thread
thread
;
Object obj;
QObject
::connect(&obj, SIGNAL(aSignal()), &
thread
, SLOT(aSlot()));
thread
.start();
obj.emitSignal();
|
當「obj」發射了一個aSignal()信號是,哪一種鏈接將被使用呢?你也許已經猜到了:direct connection。這是由於Thread對象實在發射該信號的線程中生存。在aSlot()槽裏,咱們可能接着去訪問線程裏的一些成員變量,然而這些成員變量可能同時正在被run()方法訪問:這但是致使完美災難的祕訣。可能你常常在論壇、博客裏面找到的解決方案是在線程的構造函數里加一個moveToThread(this)方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class
Thread :
public
QThread
{
Q_OBJECT
public
:
Thread() {
moveToThread(
this
);
// 錯誤
}
/* ... */
};
|
(譯註:moveToThread(this))
這樣作確實能夠工做(由於如今線程對象的依附性已經發生了改變),但這是一個很是很差的設計。這裏的錯誤在於咱們正在誤解線程對象的目的(QThread子類):QThread對象們不是線程;他們是圍繞在新產生的線程周圍用於控制管理新線程的對象,所以,它們應該用在另外一個線程(每每在它們所駐足的那一個線程)
一個比較好並且可以獲得相同結果的作法是將「工做」部分從「控制」部分剝離出來,也就是說,寫一個QObject子類並使用QObject::moveToThread()方法來改變它的線程依附性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
view plaincopy to clipboardprint?
class
Worker :
public
QObject
{
Q_OBJECT
public
slots:
void
doWork() {
/* ... */
}
};
/* ... */
QThread
thread
;
Worker worker;
connect(obj, SIGNAL(workReady()), &worker, SLOT(doWork()));
worker.moveToThread(&
thread
);
thread
.start();
|
當你須要(經過信號和槽,或者是事件、回調函數)使用一個沒有提供非阻塞式API的庫或者代碼時,爲了阻止凍結事件循環的惟一可行的解決方案是開啓一個進程或者線程。因爲建立一個新的進程的開銷顯然要比開啓一個線程的開銷大,後者每每是最多見的一種選擇。
這種API的一個很好的例子是地址解析 方法(只是想說咱們並不許備談論蹩腳的第三方API, 地址解析方法它是每一個C庫都要包含的),它負責將主機名轉化爲地址。這個過程涉及到啓動一個查詢(一般是遠程的)系統:域名系統或者叫DNS。儘管一般狀況下響應會在瞬間發生,但遠程服務器可能會失敗:一些數據包可能會丟失,網絡鏈接可能斷開等等。簡而言之,咱們也許要等待幾十秒才能獲得查詢的響應。
UNIX系統可見的標準API只有阻塞式的(不只過期的gethostbyname(3)是阻塞式的,並且更新的getservbyname(3) 以及getaddrinfo(3)也是阻塞式的)。QHostInfo [doc.qt.nokia.com], 它是一個負責處理域名查找的Qt類,該類使用了QThreadPool 從而使得查詢能夠在後臺進行)(參見here [qt.gitorious.com]);若是屏蔽了多線程支持,它將切換回到阻塞式API).
另外一個簡單的例子是圖像裝載和放大。QImageReader [doc.qt.nokia.com] 和QImage [doc.qt.nokia.com]僅僅提供了阻塞式方法來從一個設備讀取圖像,或者放大圖像到一個不一樣的分辨率。若是你正在處理一個很是大的圖像,這些處理會持續數(十)秒。
多線程容許你的程序利用多核系統的優點。由於每一個線程都是被操做系統獨立調度的,所以若是你的應用運行在這樣多核機器上,調度器極可能同時在不一樣的處理器上運行每一個線程。
例如,考慮到一個經過圖像集生成縮略圖的應用。一個_n_ threads的線程農場(也就是說,一個有着固定數量線程的線程池),在系統中可見的CPU運行一個線程(可參見QThread::idealThreadCount()),能夠將縮小圖像至縮略圖的工做交付給全部的進程,從而有效地提升了並行加速比,它與處理器的數量成線性關係。(簡單的講,咱們認爲CPU正成爲一個瓶頸)。
這是一個很高級的話題,你能夠忽略該小節。一個比較好的例子來自於Webkit裏使用的QNetworkAccessManager 。Webkit是一個時髦的瀏覽器引擎,也就是說,它是一組用於處理網頁的佈局和顯示的類集合。使用Webkit的Qt widget是QWebView。
QNetworkAccessManager 是一個用於處理HTTP任何請求和響應的Qt類,咱們能夠把它看成一個web瀏覽器的網絡引擎;全部的網絡訪問被同一個QNetworkAccessManager 以及它的QNetworkReplys 駐足的線程所處理。
儘管在網絡處理時不使用線程是一個很好的主意,它也有一個很大的缺點:若是你沒有從socket中儘快地讀取數據,內核的緩存將會被填滿,數據包可能開始丟失並且傳輸速率也將迅速降低。
Sokcet活動(即,從一個socket讀取一些數據的可見性)由Qt的事件循環管理。阻塞事件循環所以會致使傳輸性能的損失,由於沒有人會被通知將有數據能夠讀取(從而沒人會去讀數據)。
但究竟什麼會阻塞事件循環呢?使人沮喪地回答: WebKit它本身!只要有數據被接收到,WebKit便用其來佈局網頁。不幸地是,佈局處理過程至關複雜,並且開銷巨大。所以,它阻塞事件循環的一小段時間足以影響到正在進行地傳輸(寬帶鏈接這裏起到了做用,在短短几秒內就可填滿內核緩存)。
總結一下上述所發生的事情:
WebKit提出了一個請求; 一些響應數據開始到達; WebKit開始使用接收到的數據佈局網頁,從而阻塞了事件循環; 數據被OS接受,但沒有一個正在運行的事件循環爲之派發,因此並無被QNetworkAccessManager sockets所讀取; 內核緩存將被填滿,傳輸將變慢。 網頁的整體裝載時間因其自發引發的傳輸速率下降而變得愈來愈壞。
諾基亞的工程師正在試驗一個支持多線程的QNetworkAccessManager來解決這個問題。請注意由於QNetworkAccessManagers 和QNetworkReplys 是QObjects,他們不是線程安全的,所以你不能簡單地將他們移到另外一個線程中而且繼續在你的線程中使用他們,緣由在於,因爲事件將被隨後線程的事件循環所派發,他們可能同時被兩個線程訪問:你本身的線程以及已經它們駐足的線程。
If you think you need threads then your processes are too fat. — Bradley T. Hughes
這也許是線程濫用最壞的一種形式。若是咱們不得不重複調用一個方法(好比每秒),許多人會這樣作:
1
2
3
4
5
6
|
view plaincopy to clipboardprint?
// 很是之錯誤
while
(condition) {
doWork();
sleep(1);
// this is sleep(3) from the C library
}
|
而後他們發現這會阻塞事件循環,所以決定引入線程:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
view plaincopy to clipboardprint?
// 錯誤
class
Thread :
public
QThread
{
protected
:
void
run() {
while
(condition) {
// notice that "condition" may also need volatiness and mutex protection
// if we modify it from other threads (!)
doWork();
sleep(1);
// this is QThread::sleep()
}
}
};
|
一個更好也更簡單的得到相同效果的方法是使用timers,即一個QTimer[doc.qt.nokia.com]對象,並設置一秒的超時時間,並讓doWork方法成爲它的槽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
view plaincopy to clipboardprint?
class
Worker :
public
QObject
{
Q_OBJECT
public
:
Worker() {
connect(&timer, SIGNAL(timeout()),
this
, SLOT(doWork()));
timer.start(1000);
}
private
slots:
void
doWork() {
/* ... */
}
private
:
QTimer
timer;
};
|
全部咱們須要作的就是運行一個事件循環,而後doWork()方法將會被每隔秒鐘調用一次。
一個處理網絡操做很是之常見的設計模式以下:
1
2
3
4
5
6
7
8
9
10
11
12
|
view plaincopy to clipboardprint?
socket->connect(host);
socket->waitForConnected();
data = getData();
socket->write(data);
socket->waitForBytesWritten();
socket->waitForReadyRead();
socket->read(response);
reply = process(response);
socket->write(reply);
socket->waitForBytesWritten();
/* ... and so on ... */
|
不用多說,各類各樣的waitFor*()函數阻塞了調用者使其沒法返回到事件循環,UI被凍結等等。請注意上面的這段代碼並無考慮到錯誤處理,不然它會更加地笨重。這個設計中很是錯誤的地方是咱們正在忘卻網絡編程是異步的設計,若是咱們構建一個同步的處理方法,則是本身給本身找麻煩。爲了解決這個問題,許多人簡單得將這些代碼移到另外一個線程中。
另外一個更加抽象的例子:
1
2
3
4
5
6
7
8
9
10
|
view plaincopy to clipboardprint?
result = process_one_thing();
if
(result->something())
process_this();
else
process_that();
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* ... */
|
它多少反映了網絡編程相同的陷阱。
讓咱們回過頭來從更高的角度來想一下咱們這裏正在構建的代碼:咱們想創造一個狀態機,用以反映某類的輸入並相對應的做某些動做。好比,上面的這段網絡代碼,咱們可能想作以下這些事情:
空閒→ 正在鏈接 (當調用connectToHost()); 正在鏈接→ 已經鏈接(當connected() 信號被髮射); 已經鏈接→ 發送錄入數據 (當咱們發送錄入的數據給服務器); 發送錄入數據 → 錄入 (服務器響應一個ACK) 發送錄入數據→ 錄入錯誤(服務器響應一個NACK) 以此類推。
如今,有不少種方式來構建狀態機(Qt甚至提供了QStateMachine[doc.qt.nokia.com]類),最簡單的方式是用一個枚舉值(及,一個整數)來記憶當前的狀態。咱們能夠這樣重寫如下上面的代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class
Object :
public
QObject
{
Q_OBJECT
enum
State {
State1, State2, State3
/* and so on */
};
State state;
public
:
Object() : state(State1)
{
connect(source, SIGNAL(ready()),
this
, SLOT(doWork()));
}
private
slots:
void
doWork() {
switch
(state) {
case
State1:
/* ... */
state = State2;
break
;
case
State2:
/* ... */
state = State3;
break
;
/* etc. */
}
}
};
|
那麼「souce」對象和它的信號「ready()」 到底是什麼? 咱們想讓它們是什麼就是什麼:好比說,在這個例子中,咱們可能想把咱們的槽鏈接到socket的QAbstractSocket::connected() 以及QIODevice::readyRead() 信號中,固然,咱們也能夠簡單地在咱們的用例中加更多的槽(好比一個槽用於處理錯誤狀況,它將會被QAbstractSocket::error() 信號所通知)。這是一個真正的異步的,信號驅動的設計!
假如咱們有一個開銷很大的計算,它不可以輕易的移到另外一個線程中(或者說它根本不能被移動,舉個例子,它必須運行在GUI線程中)。若是咱們能將計算拆分紅小的塊,咱們就能返回到事件循環,讓它來派發事件,並讓它激活處理下一個塊相應的函數。若是咱們還記得queued connections是怎麼實現的,那麼會以爲這是很容易可以作到的:一個事件派發到接收者所駐足的線程的事件循環;當事件被傳遞,相應的槽隨之被激活。
咱們可使用特化QMetaObject::invokeMethod() 的激活類型爲Qt::QueuedConnection 來獲得相同的結果;這須要函數是可激活的。所以它須要一個槽或者用Q_INVOKABLE宏來標識。若是咱們同時想給函數中傳入參數,他們須要使用Qt元對象類型系統裏的qRegisterMetaType()進行註冊。請看下面這段代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
class
Worker :
public
QObject
{
Q_OBJECT
public
slots:
void
startProcessing()
{
processItem(0);
}
void
processItem(
int
index)
{
/* process items[index] ... */
if
(index < numberOfItems)
QMetaObject
::invokeMethod(
this
,
"processItem"
,
Qt::QueuedConnection,
Q_ARG(
int
, index + 1));
}
};
|
http://www.cnblogs.com/newstart/p/3202118.html