天天看點

事件循環與線程 二

續上文:http://blog.csdn.net/changsheng230/archive/2010/12/27/6101232.aspx

由于最近工作比較忙,出了趟差,還是把這篇長文、好文翻譯出來了,以飨讀者。同時也是自己很好的消化、學習過程

Qt 線程類

Qt對線程的支援已經有很多年了(釋出于2000年九月22日的Qt2.2引入了QThread類),Qt 4.0版本的release則對其所有所支援平台預設地是對多線程支援的。(當然你也可以關掉對線程的支援,參見這裡)。現在Qt提供了不少類用于處理線程,讓你我們首先預覽一下:

QThread

QThread 是Qt中一個對線程支援的核心的底層類。 每個線程對象代表了一個運作的線程。由于Qt的跨平台特性,QThread成功隐藏了所有在不同作業系統裡使用線程的平台相關性代碼。

為了運用QThread進而讓代碼在一個線程裡運作,我們可以建立一個QThread的子類,并重載QThread::run() 方法:

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 和 QThreadPool

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

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

線程與QObjects

線程的事件循環

我們在上文中已經讨論了事件循環,我們可能理所當然地認為在Qt的應用程式中隻有一個事件循環,但事實并不是這樣:QThread對象在它們所代表的線程中開啟了新的事件循環。是以,我們說main 事件循環是由調用main()的線程通過QCoreApplication::exec() 建立的。 它也被稱做是GUI線程,因為它是界面相關操作唯一允許的程序。一個QThread的局部事件循環可以通過調用QThread::exec() 來開啟(它包含在run()方法的内部)

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對象構造函數中建構的對象。

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對象自己作為它的雙親。 (譯者注:兩者不在同一個線程中):

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() 為我們做了如下工作:

QMetaObject::invokeMethod(object, "methodName",

Qt::QueuedConnection,

Q_ARG(type1, arg1),

Q_ARG(type2, arg2)); 

請注意,因為上面所示的參數需要被在建構事件時進行硬拷貝,參數的自定義型别所對應的類需要提供一個共有的構造函數、析構函數以及拷貝構造函數。而且必須使用注冊Qt型别系統所提供的qRegisterMetaType() 方法來注冊這一自定義型别。

跨線程的信号槽的工作方式相類似。當我們把信号連接配接到一個槽的時候,QObject::connect的第五個可選輸入參數用來特化這一連接配接類型:

  • direct connection 是指:發起信号的線程會直接觸發其所連接配接的槽;
  • queued connection 是指:一個事件被派發到接收者所在的線程中,在這裡,事件循環會之後的某一時間将該事件拾起并引起槽的調用;
  • blocking queued connection 與queued connection的差別在于,發送者的線程會被阻塞,直至接收者所線上程的事件循環處理發送者發送(入棧)的事件,當連接配接信号的槽被觸發後,阻塞被解除;
  • automatic connection (預設預設參數) 是指: 如果接收者所依附的線程和目前線程是同一個線程,direct connection會被使用。否則使用queued connection。

請注意,在上述四種連接配接方式當中,發送對象駐足于哪一個線程并不重要!對于automatic connection,Qt會檢查觸發信号的線程,并且與接收者所駐足的線程相比較進而決定到底使用哪一種連接配接類型。特别要指出的是:目前的Qt文檔的聲明(4.7.1) 是錯誤的:

如果發射者和接受者在同一線程,其行為與Direct Connection相同;,如果發射者和接受者不在同一線程,其行為Queued Connection相同

因為,發送者對象的線程依附性在這裡無關緊要。舉例子說明

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)。

另外一個常見的錯誤如下:

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)方法。

class

Thread :

public

QThread

{

Q_OBJECT

public

:

Thread() {

moveToThread(

this

);

// 錯誤

}

};

(譯注:

moveToThread(this)

這樣做确實可以工作(因為現線上程對象的依附性已經發生了改變),但這是一個非常不好的設計。這裡的錯誤在于我們正在誤解線程對象的目的(QThread子類):QThread對象們不是線程;他們是圍繞在新産生的線程周圍用于控制管理新線程的對象,是以,它們應該用在另一個線程(往往在它們所駐足的那一個線程)

一個比較好而且能夠得到相同結果的做法是将“工作”部分從“控制”部分剝離出來,也就是說,寫一個QObject子類并使用QObject::moveToThread()方法來改變它的線程依附性:

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的一個很好的例子是位址解析 方法(隻是想說我們并不準備談論蹩腳的第三方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.—Rob Pike

計時器

這也許是線程濫用最壞的一種形式。如果我們不得不重複調用一個方法(比如每秒),許多人會這樣做:

// 非常之錯誤

while (condition) {

doWork();

sleep(1); // this is sleep(3) from the C library

然後他們發現這會阻塞事件循環,是以決定引入線程:

// 錯誤

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方法成為它的槽:

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()方法将會被每隔秒鐘調用一次。

網絡/狀态機

一個處理網絡操作非常之常見的設計模式如下:

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被當機等等。請注意上面的這段代碼并沒有考慮到錯誤處理,否則它會更加地笨重。這個設計中非常錯誤的地方是我們正在忘卻網絡程式設計是異步的設計,如果我們建構一個同步的處理方法,則是自己給自己找麻煩。為了解決這個問題,許多人簡單得将這些代碼移到另一個線程中。

另一個更加抽象的例子:

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]類),最簡單的方式是用一個枚舉值(及,一個整數)來記憶目前的狀态。我們可以這樣重寫以下上面的代碼:

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()進行注冊。請看下面這段代碼:

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://developer.qt.nokia.com/wiki/ThreadsEventsQObjects

請尊重原創作品和譯文。轉載請保持文章完整性,并以超連結形式注明原始作者主站點位址,友善其他朋友提問和指正。 

繼續閱讀