綫程基礎

什麼是綫程?

Threads are about doing things in parallel, just like processes. So how do threads differ from processes? While you are making calculations on a spreadsheet, there may also be a media player running on the same desktop playing your favorite song. Here is an example of two processes working in parallel: one running the spreadsheet program; one running a media player. Multitasking is a well known term for this. A closer look at the media player reveals that there are again things going on in parallel within one single process. While the media player is sending music to the audio driver, the user interface with all its bells and whistles is being constantly updated. This is what threads are for – concurrency within one single process.

那麼,如何實現並發呢?在單核 CPU 上並行工作是一種錯覺,有點類似於電影中移動圖像的錯覺。對於進程,錯覺是在很短時間後中斷處理器在一進程中的工作而産生的。然後,處理器繼續處理下一進程。為在進程之間切換,保存當前程序計數器,並加載下一處理器的程序計數器。這還不夠,因為需要對寄存器、某些體係結構及特定 OS 數據執行相同操作。

就像一個 CPU 可以驅動 2 個或多個進程,也可以讓 CPU 運行在一個單進程的 2 個不同代碼段中。當進程啓動時,它始終執行一代碼段,因此說進程擁有一綫程。不管怎樣,程序可能決定啓動第 2 綫程。然後,在一個進程內同時處理 2 個不同的代碼序列。通過重復保存程序計數器和寄存器,然後加載下一綫程的程序計數器和寄存器,在單核 CPU 中達成並發。在活動綫程之間循環,不需要程序的閤作。當切換到下一綫程齣現時,綫程可能處於任何狀態。

CPU 設計的當前趨勢是擁有多個核心。典型的單綫程應用程序隻能使用一個核心。不管怎樣,可以將具有多個綫程的程序賦值給多個核心,從而使事情以真正的並發方式發生。結果,將工作分發給多個綫程可以使程序在多核 CPU 上運行得更快,因為可以使用其它核心。

GUI 綫程和工作綫程

如前所述,每個程序擁有一綫程當它啓動時。該綫程被稱為主綫程 (在 Qt 應用程序中又稱為 GUI 綫程)。Qt GUI 必須在此綫程中運行。所有 Widget 和幾個相關類,例如 QPixmap ,不工作於第 2 綫程。第 2 綫程通常稱為工作者綫程,因為它用於從主綫程分擔處理工作。

同時訪問數據

每個綫程擁有自己的堆棧,意味著每個綫程擁有自己的調用曆史和局部變量。不像進程,綫程共享相同地址空間。以下簡圖展示如何在內存中定位綫程構造塊。非活動綫程的程序計數器和寄存器通常保持在內核空間中。有共享代碼副本,且每個綫程有單獨堆棧。

"Thread visualization"

若 2 綫程擁有相同對象指針,則 2 綫程同時訪問該對象是可能的,且這可能潛在破壞對象的完整性。很容易想象很多事情可能齣錯,當同一對象的 2 方法同時執行時。

有時有必要從不同綫程訪問某一對象;例如,當活在不同綫程中的對象需要通信時。由於綫程使用相同地址空間,綫程交換數據更容易且更快,相比進程。不必序列化和拷貝數據。傳遞指針是可能的,但必須嚴格協調什麼綫程接觸哪個對象。必須防止在一對象上同時執行操作。有幾種辦法能達成這且下文將描述其中一些辦法。

那麼,怎樣做纔安全呢?可以安全地使用在綫程中創建的所有對象在該綫程中,前提是其它綫程沒有它的引用且對象沒有隱式耦閤其它綫程。這種隱式耦閤可能發生,當采用靜態成員、單例或全局數據在實例之間共享數據時。熟悉的概念是 綫程安全和可重入 類和函數。

使用綫程

基本上,綫程有 2 種使用案例:

  • 利用多核處理器提高處理速度。
  • 保持 GUI 綫程或其它時間臨界綫程響應,通過分擔長時間處理或阻塞其它綫程的調用。

何時使用綫程替代

開發者采用綫程時需要很小心。啓動其它綫程很容易,但很難確保所有共享數據仍然一緻。問題經常難以發現,因為它們可能僅偶爾齣現一次,或僅在特定硬件配置上齣現。在創建綫程解決某些問題前,應考慮可能的替代。

Alternative 注釋
QEventLoop::processEvents () 調用 QEventLoop::processEvents () 重復在耗時計算期間防止 GUI 阻塞。然而,此解決方案伸縮性不好,因為調用 processEvents() 發生次數可能過多或不足,從屬硬件。
QTimer 有時可以使用計時器方便履行後颱處理,以在將來某個時間點調度槽的執行。0 間隔計時器將盡快超時,一旦沒有更多要處理的事件。
QSocketNotifier QNetworkAccessManager QIODevice::readyRead () 這是擁有一個或多個綫程的替代,每個在緩慢網絡連接上阻塞讀取。隻要可以快速執行響應網絡數據組塊的計算,這種反應式設計就比等待綫程同步更優。反應式設計比綫程更不易於齣錯且高效節能。在許多情況下,還有性能好處。

一般而言,隻推薦使用安全且經過測試的路綫,避免引入特彆綫程概念。 QtConcurrent 模塊提供簡易接口以將工作分發到所有處理器核心。 綫程代碼完全隱藏在 QtConcurrent 框架,因此,不必關心細節。不管怎樣, QtConcurrent 不可以使用當需要與正運行綫程通信時,且不應將其用於處理阻塞操作。

應使用哪種 Qt 綫程技術?

Qt 中的多綫程技術 頁麵瞭解 Qt 多綫程的不同介紹方式,及如何選擇它們的有關指導方針。

Qt 綫程基礎

以下章節描述 QObject 如何與綫程交互,程序如何安全地從多個綫程訪問數據,及異步執行如何不阻塞綫程産生結果。

QObject 和綫程

如上所述,開發者必須始終小心當從其它綫程調用對象方法時。 綫程親緣關係 不改變此狀況。Qt 文檔編製將幾種方法標記為綫程安全。 postEvent () is a noteworthy example. A thread-safe method may be called from different threads simultaneously.

通常,在沒有並發訪問方法的情況下,在其它綫程中調用對象的非綫程安全方法工作數韆次,在並發訪問齣現之前,可能導緻意外行為。編寫測試代碼並不能完全確保綫程的正確性,但仍很重要。在 Linux,Valgrind 和 Helgrind 可以幫助檢測綫程錯誤。

保護數據完整性

在編寫多綫程應用程序時,必須小心避免數據破壞。見 同步綫程 瞭解如何安全使用綫程的有關討論。

處理異步執行

獲得工作者綫程結果的一種辦法是等待綫程終止。然而,在很多情況下,阻塞等待不可接受。阻塞等待的替代是采用發布事件、隊列信號及槽異步交付結果。這會産生某些開銷,因為操作結果不會齣現在下一源代碼行中,而是齣現在定位源代碼文件中某些位置的槽中。Qt 開發者習慣使用這種異步行為,因為它非常類似用於 GUI 應用程序的事件驅動編程。

範例

Qt 帶有使用綫程的一些範例。見類參考對於 QThread and QThreadPool 瞭解簡單範例。見 綫程和並發編程範例 頁麵瞭解更高級的。

深入挖掘

綫程是很復雜的主題。Qt 提供更多綫程類,相比在此教程中呈現的。以下材料可幫您更深入研究該主題:

  • The Thread Support in Qt document is a good starting point into the reference documentation.
  • Qt 帶有一些額外範例對於 QThread 和 QtConcurrent .