綫程是並行地做事情,僅僅像進程。那麼,綫程與進程有何不同?若在電子錶格中做計算,可能還有媒體播放器在同一桌麵中運行,播放您最喜愛的歌麯。這是 2 個進程的並行工作範例:一個運行電子錶格程序;另一個運行媒體播放器。為此,多任務處理是眾所周知的術語。仔細觀察媒體播放器會發現,在一個單進程中還有事情在並行。當媒體播放器將音樂發送給音頻驅動器時,具有所有花哨功能的用戶界麵還在不斷更新。這就是綫程 -- 在一個單進程內並發。
那麼,如何實現並發呢?在單核 CPU 上並行工作是一種錯覺,有點類似於電影中移動圖像的錯覺。對於進程,錯覺是在很短時間後中斷處理器在一進程中的工作而産生的。然後,處理器繼續處理下一進程。為在進程之間切換,保存當前程序計數器,並加載下一處理器的程序計數器。這還不夠,因為需要對寄存器、某些體係結構及特定 OS 數據執行相同操作。
就像一個 CPU 可以驅動 2 個或多個進程,也可以讓 CPU 運行在一個單進程的 2 個不同代碼段中。當進程啓動時,它始終執行一代碼段,因此說進程擁有一綫程。不管怎樣,程序可能決定啓動第 2 綫程。然後,在一個進程內同時處理 2 個不同的代碼序列。通過重復保存程序計數器和寄存器,然後加載下一綫程的程序計數器和寄存器,在單核 CPU 中達成並發。在活動綫程之間循環,不需要程序的閤作。當切換到下一綫程齣現時,綫程可能處於任何狀態。
CPU 設計的當前趨勢是擁有多個核心。典型的單綫程應用程序隻能使用一個核心。不管怎樣,可以將具有多個綫程的程序賦值給多個核心,從而使事情以真正的並發方式發生。結果,將工作分發給多個綫程可以使程序在多核 CPU 上運行得更快,因為可以使用其它核心。
如前所述,每個程序擁有一綫程當它啓動時。該綫程被稱為主綫程 (在 Qt 應用程序中又稱為 GUI 綫程)。Qt GUI 必須在此綫程中運行。所有 Widget 和幾個相關類,例如 QPixmap ,不工作於第 2 綫程。第 2 綫程通常稱為工作者綫程,因為它用於從主綫程分擔處理工作。
每個綫程擁有自己的堆棧,意味著每個綫程擁有自己的調用曆史和局部變量。不像進程,綫程共享相同地址空間。以下簡圖展示如何在內存中定位綫程構造塊。非活動綫程的程序計數器和寄存器通常保持在內核空間中。有共享代碼副本,且每個綫程有單獨堆棧。
若 2 綫程擁有相同對象指針,則 2 綫程同時訪問該對象是可能的,且這可能潛在破壞對象的完整性。很容易想象很多事情可能齣錯,當同一對象的 2 方法同時執行時。
有時有必要從不同綫程訪問某一對象;例如,當活在不同綫程中的對象需要通信時。由於綫程使用相同地址空間,綫程交換數據更容易且更快,相比進程。不必序列化和拷貝數據。傳遞指針是可能的,但必須嚴格協調什麼綫程接觸哪個對象。必須防止在一對象上同時執行操作。有幾種辦法能達成這且下文將描述其中一些辦法。
那麼,怎樣做纔安全呢?可以安全地使用在綫程中創建的所有對象在該綫程中,前提是其它綫程沒有它的引用且對象沒有隱式耦閤其它綫程。這種隱式耦閤可能發生,當采用靜態成員、單例或全局數據在實例之間共享數據時。熟悉的概念是 綫程安全和可重入 類和函數。
基本上,綫程有 2 種使用案例:
開發者采用綫程時需要很小心。啓動其它綫程很容易,但很難確保所有共享數據仍然一緻。問題經常難以發現,因為它們可能僅偶爾齣現一次,或僅在特定硬件配置上齣現。在創建綫程解決某些問題前,應考慮可能的替代。
| Alternative | 注釋 |
|---|---|
| QEventLoop::processEvents () | 調用 QEventLoop::processEvents () 重復在耗時計算期間防止 GUI 阻塞。然而,此解決方案伸縮性不好,因為調用 processEvents() 發生次數可能過多或不足,從屬硬件。 |
| QTimer | 有時可以使用計時器方便履行後颱處理,以在將來某個時間點調度槽的執行。0 間隔計時器將盡快超時,一旦沒有更多要處理的事件。 |
| QSocketNotifier QNetworkAccessManager QIODevice::readyRead () | 這是擁有一個或多個綫程的替代,每個在緩慢網絡連接上阻塞讀取。隻要可以快速執行響應網絡數據組塊的計算,這種反應式設計就比等待綫程同步更優。反應式設計比綫程更不易於齣錯且高效節能。在許多情況下,還有性能好處。 |
一般而言,隻推薦使用安全且經過測試的路綫,避免引入特彆綫程概念。 QtConcurrent 模塊提供簡易接口以將工作分發到所有處理器核心。 綫程代碼完全隱藏在 QtConcurrent 框架,因此,不必關心細節。不管怎樣, QtConcurrent 不可以使用當需要與正運行綫程通信時,且不應將其用於處理阻塞操作。
見 Qt 中的多綫程技術 頁麵瞭解 Qt 多綫程的不同介紹方式,及如何選擇它們的有關指導方針。
以下章節描述 QObject 如何與綫程交互,程序如何安全地從多個綫程訪問數據,及異步執行如何不阻塞綫程産生結果。
如上所述,開發者必須始終小心當從其它綫程調用對象方法時。 綫程親緣關係 不改變此狀況。Qt 文檔編製將幾種方法標記為綫程安全。 postEvent() 是顯著範例。可以從不同綫程同時調用綫程安全方法。
通常,在沒有並發訪問方法的情況下,在其它綫程中調用對象的非綫程安全方法工作數韆次,在並發訪問齣現之前,可能導緻意外行為。編寫測試代碼並不能完全確保綫程的正確性,但仍很重要。在 Linux,Valgrind 和 Helgrind 可以幫助檢測綫程錯誤。
在編寫多綫程應用程序時,必須小心避免數據破壞。見 同步綫程 瞭解如何安全使用綫程的有關討論。
獲得工作者綫程結果的一種辦法是等待綫程終止。然而,在很多情況下,阻塞等待不可接受。阻塞等待的替代是采用發布事件、隊列信號及槽異步交付結果。這會産生某些開銷,因為操作結果不會齣現在下一源代碼行中,而是齣現在定位源代碼文件中某些位置的槽中。Qt 開發者習慣使用這種異步行為,因為它非常類似用於 GUI 應用程序的事件驅動編程。
Qt 帶有使用綫程的一些範例。見類參考對於 QThread and QThreadPool 瞭解簡單範例。見 綫程和並發編程範例 頁麵瞭解更高級的。
綫程是很復雜的主題。Qt 提供更多綫程類,相比在此教程中呈現的。以下材料可幫您更深入研究該主題: