有關線程,你能夠作的是:git
- 在
QThread
子類添加信號。這是絕對安全的,而且也是正確的(前面咱們已經詳細介紹過,發送者的線程依附性沒有關係)
不該該作的是:github
- 調用
moveToThread(this)
函數 - 指定鏈接類型:這一般意味着你正在作錯誤的事情,好比將
QThread
控制接口與業務邏輯混雜在了一塊兒(而這應該放在該線程的一個獨立對象中) - 在
QThread
子類添加槽函數:這意味着它們將在錯誤的線程被調用,也就是QThread
對象所在線程,而不是QThread
對象管理的線程。這又須要你指定鏈接類型或者調用moveToThread(this)
函數 - 使用
QThread::terminate()
函數
不能作的是:設計模式
- 在線程還在運行時退出程序。使用
QThread::wait()
函數等待線程結束 - 在
QThread
對象所管理的線程仍在運行時就銷燬該對象。若是你須要某種「自行銷燬」的操做,你能夠把finished()
信號同deleteLater()
槽鏈接起來
那麼,下面一個問題是:我何時應該使用線程?瀏覽器
首先,當你不得不使用同步 API 的時候。安全
若是你須要使用一個沒有非阻塞 API 的庫或代碼(所謂非阻塞 API,很大程度上就是指信號槽、事件、回調等),那麼,避免事件循環被阻塞的解決方案就是使用進程或者線程。不過,因爲開啓一個新的工做進程,讓這個進程去完成任務,而後再與當前進程進行通訊,這一系列操做的代價都要比開啓線程要昂貴得多,因此,線程一般是最好的選擇。服務器
一個很好的例子是地址解析服務。注意咱們這裏並不討論任何第三方 API,僅僅假設一個有這樣功能的庫。這個庫的工做是將一個主機名轉換成地址。這個過程須要去到一個系統(也就是域名系統,Domain Name System, DNS)執行查詢,這個系統一般是一個遠程系統。通常這種響應應該瞬間完成,可是並不排除遠程服務器失敗、某些包可能會丟失、網絡可能失去連接等等。簡單來講,咱們的查詢可能會等幾十秒鐘。網絡
UNIX 系統上的標準 API 是阻塞的(不只是舊的gethostbyname(3)
,就連新的getservbyname(3)
和getaddrinfo(3)
也是同樣)。Qt 提供的QHostInfo
類一樣用於地址解析,默認狀況下,內部使用一個QThreadPool
提供後臺運行方式的查詢(若是關閉了 Qt 的線程支持,則提供阻塞式 API)。異步
另一個例子是圖像加載和縮放。QImageReader
和QImage
只提供了阻塞式 API,容許咱們從設備讀取圖片,或者是縮放到不一樣的分辨率。若是你須要處理很大的圖像,這種任務會花費幾十秒鐘。socket
其次,當你但願擴展到多核應用的時候。ide
線程容許你的程序利用多核系統的優點。每個線程均可以被操做系統獨立調度,若是你的程序運行在多核機器上,調度器極可能會將每個線程分配到各自的處理器上面運行。
舉個例子,一個程序須要爲不少圖像生成縮略圖。一個具備固定 n 個線程的線程池,每個線程交給系統中的一個可用的 CPU 進行處理(咱們可使用QThread::idealThreadCount()
獲取可用的 CPU 數)。這樣的調度將會把圖像縮放工做交給全部線程執行,從而有效地提高效率,幾乎達到與 CPU 數的線性提高(實際狀況不會這麼簡單,由於有時候 CPU 並非瓶頸所在)。
第三,當你不想被別人阻塞的時候。
這是一個至關高級的話題,因此你如今能夠暫時不看這段。這個問題的一個很好的例子是在 WebKit 中使用QNetworkAccessManager
。WebKit 是一個現代的瀏覽器引擎。它幫助咱們展現網頁。Qt 中的QWebView
就是使用的 WebKit。
QNetworkAccessManager
則是 Qt 處理 HTTP 請求和響應的通用類。咱們能夠將它看作瀏覽器的網絡引擎。在 Qt 4.8 以前,這個類沒有使用任何協助工做線程,全部的網絡處理都是在QNetworkAccessManager
及其QNetworkReply
所在線程完成。
雖然在網絡處理中不使用線程是一個好主意,但它也有一個很大的缺點:若是你不能及時從 socket 讀取數據,內核緩衝區將會被填滿,因而開始丟包,傳輸速度將會直線降低。
socket 活動(也就是從一個 socket 讀取一些可用的數據)是由 Qt 的事件循環管理的。所以,阻塞事件循環將會致使傳輸性能的損失,由於沒有人會得到有數據可讀的通知,所以也就沒有人可以讀取這些數據。
可是什麼會阻塞事件循環?最壞的答案是:WebKit 本身!只要收到數據,WebKit 就開始生成網頁佈局。不幸的是,這個佈局的過程很是複雜和耗時,所以它會阻塞事件循環。儘管阻塞時間很短,可是足以影響到正常的數據傳輸(寬帶鏈接在這裏發揮了做用,在很短期內就能夠塞滿內核緩衝區)。
總結一下上面所說的內容:
- WebKit 發起一次請求
- 從服務器響應獲取一些數據
- WebKit 利用到達的數據開始進行網頁佈局,阻塞事件循環
- 因爲事件循環被阻塞,也就沒有了可用的事件循環,因而操做系統接收了到達的數據,可是卻不能從
QNetworkAccessManager
的 socket 讀取 - 內核緩衝區被填滿,傳輸速度變慢
網頁的總體加載時間被自身的傳輸速度的下降而變得愈來愈壞。
注意,因爲QNetworkAccessManager
和QNetworkReply
都是QObject
,因此它們都不是線程安全的,所以你不能將它們移動到另外的線程繼續使用。由於它們可能同時有兩個線程訪問:你本身的和它們所在的線程,這是由於派發給它們的事件會由後面一個線程的事件循環發出,但你不能肯定哪一線程是「後面一個」。
Qt 4.8 以後,QNetworkAccessManager
默認會在一個獨立的線程處理 HTTP 請求,因此致使 GUI 失去響應以及操做系統緩衝區過快填滿的問題應該已經被解決了。
那麼,什麼狀況下不該該使用線程呢?
定時器
這多是最容易誤用線程的狀況了。若是咱們須要每隔一段時間調用一個函數,不少人可能會這麼寫代碼:
1
2
3
4
5
|
// 最錯誤的代碼
while (condition) {
doWork();
sleep(1); // C 庫裏面的 sleep(3) 函數
}
|
當讀過咱們前面的文章以後,可能又會引入線程,改爲這樣的代碼:
1
2
3
4
5
6
7
8
9
10
11
|
// 錯誤的代碼
class Thread : public QThread {
protected:
void run() {
while (condition) {
// 注意,若是咱們要在別的線程修改 condition,那麼它也須要加鎖
doWork();
sleep(1); // 此次是 QThread::sleep()
}
}
};
|
最好最簡單的實現是使用定時器,好比QTimer
,設置 1s 超時,而後將doWork()
做爲槽:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
13
14
15
|
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();
/* ... */
|
在通過前面幾章的介紹以後,不用多說,咱們就會發現這裏的問題:大量的waitFor*()
函數會阻塞事件循環,凍結 UI 界面等等。注意,上面的代碼尚未加入異常處理,不然的話確定會更復雜。這段代碼的錯誤在於,咱們的網絡實際是異步的,若是咱們非得按照同步方式處理,就像拿起槍打本身的腳。爲了解決這個問題,不少人會簡單地將這段代碼移動到一個新的線程。
一個更抽象的例子是:
1
2
3
4
5
6
7
8
9
10
11
12
|
result = process_one_thing();
if (result->something()) {
process_this();
} else {
process_that();
}
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* ... */
|
這段抽象的代碼與前面網絡的例子有「殊途同歸之妙」。
讓咱們回過頭來看看這段代碼到底是作了什麼:咱們實際是想建立一個狀態機,這個狀態機要根據用戶的輸入做出合理的響應。例如咱們網絡的例子,咱們實際是想要構建這樣的東西:
1
2
3
4
5
|
空閒
→ 正在鏈接(調用<code>connectToHost()</code>)
正在鏈接
→ 成功鏈接(發出<code>connected()</code>信號)
成功鏈接
→ 發送登陸數據(將登陸數據發送到服務器)
發送登陸數據
→ 登陸成功(服務器返回 ACK)
發送登陸數據
→ 登陸失敗(服務器返回 NACK)
|
以此類推。
既然知道咱們的實際目的,咱們就能夠修改代碼來建立一個真正的狀態機(Qt 甚至提供了一個狀態機類:QStateMachine
)。建立狀態機最簡單的方法是使用一個枚舉來記住當前狀態。咱們能夠編寫以下代碼:
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 /* ... */
};
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;
/* ... */
}
}
};
|
source
對象是哪來的?這個對象其實就是咱們關心的對象:例如,在網絡的例子中,咱們可能但願把 socket 的QAbstractSocket::connected()
或者QIODevice::readyRead()
信號與咱們的槽函數鏈接起來。固然,咱們很容易添加更多更合適的代碼(好比錯誤處理,使用QAbstractSocket::error()
信號就能夠了)。這種代碼是真正異步、信號驅動的設計。
將任務分割成若干部分
假設咱們有一個很耗時的計算,咱們不能簡單地將它移動到另外的線程(或者是咱們根本沒法移動它,好比這個任務必須在 GUI 線程完成)。若是咱們將這個計算任務分割成小塊,那麼咱們就能夠及時返回事件循環,從而讓事件循環繼續派發事件,調用處理下一個小塊的函數。回一下如何實現隊列鏈接,咱們就能夠輕鬆完成這個任務:將事件提交到接收對象所在線程的事件循環;當事件發出時,響應函數就會被調用。
咱們可使用QMetaObject::invokeMethod()
函數,經過指定Qt::QueuedConnection
做爲調用類型來達到相同的效果。不過這要求函數必須是內省的,也就是說這個函數要麼是一個槽函數,要麼標記有Q_INVOKABLE
宏。若是咱們還須要傳遞參數,咱們須要使用qRegisterMetaType()
函數將參數註冊到 Qt 元類型系統。下面是代碼示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
class Worker : public QObject
{
Q_OBJECT
public slots:
void startProcessing()
{
processItem(0);
}
void processItem(int index)
{
/* 處理 items[index] ... */
if (index < numberOfItems) {
QMetaObject::invokeMethod(this,
"processItem",
Qt::QueuedConnection,
Q_ARG(int, index + 1));
}
}
};
|
因爲沒有任何線程調用,因此咱們能夠輕易對這種計算任務執行暫停/恢復/取消,以及獲取結果。
至此,咱們利用五個章節將有關線程的問題簡單介紹了下。線程應該說是所有設計裏面最複雜的部分之一,因此這部份內容也會比較困難。在實際運用中確定會更多的問題,這就只能讓咱們具體分析了。