Qt 學習之路:線程總結

前面咱們已經詳細介紹過有關線程的一些值得注意的事項。如今咱們開始對線程作一些總結。

有關線程,你能夠作的是: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)。異步

另一個例子是圖像加載和縮放。QImageReaderQImage只提供了阻塞式 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 讀取
  • 內核緩衝區被填滿,傳輸速度變慢

網頁的總體加載時間被自身的傳輸速度的下降而變得愈來愈壞。

注意,因爲QNetworkAccessManagerQNetworkReply都是QObject,因此它們都不是線程安全的,所以你不能將它們移動到另外的線程繼續使用。由於它們可能同時有兩個線程訪問:你本身的和它們所在的線程,這是由於派發給它們的事件會由後面一個線程的事件循環發出,但你不能肯定哪一線程是「後面一個」。

Qt 4.8 以後,QNetworkAccessManager默認會在一個獨立的線程處理 HTTP 請求,因此致使 GUI 失去響應以及操做系統緩衝區過快填滿的問題應該已經被解決了。

那麼,什麼狀況下不該該使用線程呢?

定時器

這多是最容易誤用線程的狀況了。若是咱們須要每隔一段時間調用一個函數,不少人可能會這麼寫代碼:

當讀過咱們前面的文章以後,可能又會引入線程,改爲這樣的代碼:

最好最簡單的實現是使用定時器,好比QTimer,設置 1s 超時,而後將doWork()做爲槽:

咱們所須要的就是開始事件循環,而後每隔一秒doWork()就會被自動調用。

網絡/狀態機

下面是一個很常見的處理網絡操做的設計模式:

在通過前面幾章的介紹以後,不用多說,咱們就會發現這裏的問題:大量的waitFor*()函數會阻塞事件循環,凍結 UI 界面等等。注意,上面的代碼尚未加入異常處理,不然的話確定會更復雜。這段代碼的錯誤在於,咱們的網絡實際是異步的,若是咱們非得按照同步方式處理,就像拿起槍打本身的腳。爲了解決這個問題,不少人會簡單地將這段代碼移動到一個新的線程。

一個更抽象的例子是:

這段抽象的代碼與前面網絡的例子有「殊途同歸之妙」。

讓咱們回過頭來看看這段代碼到底是作了什麼:咱們實際是想建立一個狀態機,這個狀態機要根據用戶的輸入做出合理的響應。例如咱們網絡的例子,咱們實際是想要構建這樣的東西:

以此類推。

既然知道咱們的實際目的,咱們就能夠修改代碼來建立一個真正的狀態機(Qt 甚至提供了一個狀態機類:QStateMachine)。建立狀態機最簡單的方法是使用一個枚舉來記住當前狀態。咱們能夠編寫以下代碼:

source對象是哪來的?這個對象其實就是咱們關心的對象:例如,在網絡的例子中,咱們可能但願把 socket 的QAbstractSocket::connected()或者QIODevice::readyRead()信號與咱們的槽函數鏈接起來。固然,咱們很容易添加更多更合適的代碼(好比錯誤處理,使用QAbstractSocket::error()信號就能夠了)。這種代碼是真正異步、信號驅動的設計。

將任務分割成若干部分

假設咱們有一個很耗時的計算,咱們不能簡單地將它移動到另外的線程(或者是咱們根本沒法移動它,好比這個任務必須在 GUI 線程完成)。若是咱們將這個計算任務分割成小塊,那麼咱們就能夠及時返回事件循環,從而讓事件循環繼續派發事件,調用處理下一個小塊的函數。回一下如何實現隊列鏈接,咱們就能夠輕鬆完成這個任務:將事件提交到接收對象所在線程的事件循環;當事件發出時,響應函數就會被調用。

咱們可使用QMetaObject::invokeMethod()函數,經過指定Qt::QueuedConnection做爲調用類型來達到相同的效果。不過這要求函數必須是內省的,也就是說這個函數要麼是一個槽函數,要麼標記有Q_INVOKABLE宏。若是咱們還須要傳遞參數,咱們須要使用qRegisterMetaType()函數將參數註冊到 Qt 元類型系統。下面是代碼示例:

因爲沒有任何線程調用,因此咱們能夠輕易對這種計算任務執行暫停/恢復/取消,以及獲取結果。

至此,咱們利用五個章節將有關線程的問題簡單介紹了下。線程應該說是所有設計裏面最複雜的部分之一,因此這部份內容也會比較困難。在實際運用中確定會更多的問題,這就只能讓咱們具體分析了。

相關文章
相關標籤/搜索