(轉自:http://my.oschina.net/laopiao/blog/88158)算法
何謂線程?編程
線程與並行處理任務息息相關,就像進程同樣。那麼,線程與進程有什麼區別呢?當你在電子表格上進行數據結算的時候,在相同的桌面上可能有一個播放器正在播放你最喜歡的歌曲。這是一個兩個進程並行工做的例子:一個進程運行電子表格程序;另外一個進程運行一個媒體播放器。這種狀況最適合用多任務這個詞來描述。進一步觀察媒體播放器,你會發如今這個進程內,又存在並行的工做。當媒體播放器向音頻驅動發送音樂數據的時候,用戶界面上與之相關的信息不斷地進行更新。這就是單個進程內的並行線程。安全
那麼,線程的並行性是如何實現的呢?在單核CPU計算機上,並行工做相似在電影院中不停移動圖像產生的一種假象。對於進程而言,在很短的時間內中斷佔有處理器的進程就造成了這種假象。然而,處理器遷移到下一個進程。爲了在不一樣進程之間進行切換,當前程序計算器被保存,下一個程序計算器被加載進來。這還不夠,相關寄存器以及一些體系結構和操做系統特定的數據也要進行保存和從新加載。網絡
就像一個CPU能夠支撐兩個或多個進程同樣,一樣也可讓CPU在單個進程內運行不一樣的代碼片斷。當一個進程啓動時,它問題執行一個代碼片段從而該進程就被認爲是擁有了一個線程。可是,該程序能夠會決定啓動第二個線程。這樣,在一個進程內部,兩個不一樣的代碼序列就須要被同步處理。經過不停地保存當前線程的程序計數器和相關寄存器,同時加載下一個線程的程序計數器和相關寄存器,就能夠在單核CPU上實現並行。在不一樣活躍線程之間的切換不須要這些線程之間的任何協做。當切換到下一個線程時,當前線程可能處於任一種狀態。多線程
當前CPU設計的趨勢是擁有多個核。一個典型的單線程應用程序只能利用一個核。可是,一個多線程程序可被分配給多個核,便得程序以一種徹底並行的方式運行。這樣,將一個任務分配給多個線程使得程序在多核CPU計算機上的運行速度比傳統的單核CPU計算機上的運行速度快不少。併發
GUI 線程和工做者線程app
如上所述,每一個程序啓動後就會擁有一個線程。該線程稱爲」主線程」(在Qt應用程序中也叫」GUI線程」)。Qt GUI必須運行在此線程上。全部的圖形元件和幾個相關的類,如QPixmap,不能工做於非主線程中。非主線程一般稱爲」工做者線程」,由於它主要處理從主線程中卸下的一些工做。框架
數據的同步訪問異步
每一個線程都有本身的棧,這意味着每一個線程都擁有本身的調用歷史和本地變量。不一樣於進程,同一進程下的線程之間共享相同的地址空間。下圖顯示了內存中的線程塊圖。非活躍線程的程序計數器和相關寄存器一般保存在內核空間中。對每一個線程來講,存在一個共享的代碼片斷和一個單獨的棧。函數
若是兩個線程擁有一個指向相同對象的指針,那麼兩個線程能夠同時去訪問該對象,這能夠破壞該對象的完整性。很容易想象的情形是一個對象的兩個方法同時執行可能會出錯。
有時,從不一樣線程中訪問一個對象是不可避免的。例如,當位於不一樣線程中的許多對象之間須要進行通訊時。因爲線程之間使用相同的地址空間,線程之間進行數據交換要比進程之間進行數據交換快得多。數據不須要序列化而後拷貝。線程之間傳遞指針是容許的,可是必須嚴格協調哪些線程使用哪些指針。禁止在同一對象上執行同步操做。有一些方法能夠實現這種要求,下面描述其中的一些方法。
那麼,怎樣作才安全呢?在一個線程中建立的全部對象在線程內部使用是安全的,前提條件是其餘線程沒有引用該線程中建立的一些對象且這些對象與其餘的線程之間沒有隱性耦合關係。當數據做爲靜態成員變量,單例或全局數據方式共享時,這種隱性耦合是可能發生的。
使用線程
基本上,對線程來說,有兩種使用情形:
· 利用多核處理器使處理速度更快。
· 將一些處理時間較長或阻塞的任務移交給其餘的線程,從而保證GUI線程或其餘對時間敏感的線程保持良好的反應速度。
什麼時候不該使用線程
開發者在使用線程時必須特地當心。啓動其餘線程很容易,但很難保證全部共享的數據仍然是一致的。這些問題一般很難找到,由於它們能夠在某個時候僅顯示一次或僅在某種硬件配置下出現。在建立線程解決某些問題以前,以下的一些方法也應該考慮一下。
非線程方式 |
說明 |
QEventLoop::processEvents() |
在一個耗時的計算中不停地調用QEventLoop::processEvents()能以避免GUI被阻塞。可是,這種解決方式並不能用於更大範圍的計算操做中,由於會致使調用 processEvents()太頻繁或不夠,取決於硬件。. |
QTimer |
有時,在後臺進程中使用一個計時器來調度在未來某個時間點運行一段程序很是方便。超時時間爲0的計時器將在事件處理完後當即觸發。 |
QSocketNotifierQNetworkAccessManagerQIODevice::readyRead() |
當在一個低速的網絡鏈接上進行阻塞讀的時候,能夠不使用多線程。只要對一塊網絡數據的計算能夠很快地執行,那麼,這種交互式的設計比線程中的同步等待要好些。交互式設計比多線程要不容易出錯且更有效。在許多狀況下,也有一些性能上的提高。 |
通常來說,建議只使用安全的且已被驗證過的路徑,避免引入線程概念。 QtConcurrent提供了一種簡易的接口,來將工做分配到全部的處理器的核上。線程相關代碼已經徹底隱藏在QtConcurrent 框架中,所以,開發者不須要關注這些細節。可是, QtConcurrent 不能用於那麼須要與運行中的線程進行通訊的情形,且它也不能用於處理阻塞操做。
該使用哪一種 Qt 線程技術?
有時,咱們不只僅只是在另外一個線程中運行一個方法。可能須要位於其餘線程中的某個對象爲GUI線程提供服務。也許,你想其餘的線程一直保持活躍狀態去不停地輪詢硬件端口並在一個須要關注的事件發生時發送一個信號給GUI線程。Qt提供了不一樣的解決方案來開發多線程應用程序。正確的解決方案取決於新線程的目的以及它的生命週期。
線程的生命週期 |
開發任務 |
解決方案 |
單次調用 |
在其餘的線程中運行一個方法,當方法運行結束後退出線程。 |
Qt 提供了不一樣的解決方案: · 1.編寫一個函數,而後利用 QtConcurrent::run()運行它。 · 2.從QRunnable 派生一個類,並利用全局線程池QThreadPool::globalInstance()->start()來運行它。 · 3. 從QThread派生一個類, 重載QThread::run() 方法並使用QThread::start()來運行它。 |
單次調用 |
在容器中的全部項執行相同的一些操做。執行過程當中使用全部可用的核。一個通用的例子就是從一個圖像列表中產生縮略圖。 |
QtConcurrent 提供了 map()函數來將這些操做應用於於容器中的每一個項中,filter() 用於選擇容器元素,以及指定一個刪減函數的選項來與容器中剩下的元素進行合併。 |
單次調用 |
一個耗時的操做必須放到另外一個線程中運行。在這期間,狀態信息必須發送到GUI線程中。 |
使用 QThread,,重載run方法並根據狀況發送信號。.使用queued信號/槽鏈接來鏈接信號與GUI線程的槽。 |
常駐 |
有一對象位於另外一個線程中,將讓其根據不一樣的請求執行不一樣的操做。這意味與工做者線程之間的通訊是必須的。 |
從QObject 派生一個類並實現必要的槽和信號,將對象移到一個具備事件循環的線程中,並經過queued信號/槽鏈接與對象進行通訊。 |
常駐 |
對象位於另外一個線程中,對象不斷執行重複的任務如輪詢某個端口,並與GUI線程進行通訊。 |
與上述相似,但同時在工做者線程中使用一個計時器來實現輪詢。可是,最好的解決方案是徹底避免輪詢。有時,使用 QSocketNotifier 是一種不錯的選擇。 |
Qt 線程基礎
QThread 是對本地平臺線程的一個很是好的跨平臺抽象。啓動一個線程很是簡單。讓咱們看一段代碼,它產生另外一個線程,該線程打印hello,而後退出。
咱們從QThread 中派生一個類並重載run()方法。
run方法中包含的代碼會運行於一個單獨的線程。在本例中,一條包含線程ID的信號將會被輸出來。QThread::start() 會在另外一個線程中調用該方法。
爲了啓動該線程,咱們的線程對象必須被初始化。start() 方法建立了一個新的線程並在新線程中調用重載的run() 方法。 在 start() 被調用後,有兩個程序計數器走過程序代碼。主函數啓動,且僅有一個GUI線程運行,它中止時也只有一個GUI線程運行。當另外一個線程仍然忙碌時退出程序是一種編程錯誤,所以, wait方法被調用用來阻塞調用的線程直到run()方法執行完畢。
下面是運行代碼的結果:
hello from GUI thread 3079423696
hello from worker thread 3076111216
QObject 和線程
一個 QObject 一般被認爲有線程親和力 或換句話說, 它位於某個線程中。這意味着,在建立的時候, QObject保存了一個指向當前線程的指針。當一個事件利用 postEvent()發出時,該信息就變得有關了。該事件將會被放於對應線程的事件循環中。若是QObject位於的線程沒有事件循環,那麼事件就不會被傳遞。
爲了啓動一個事件循環,exec() 必須在 run()裏面調用. 線程親和力可以使用moveToThread()來改變。如上所述,開發者從其餘線程中調用對象的方法時必須很是當心。線程親和力並無改變這種情況。Qt文檔標記了幾個方法是線程安全的。 postEvent() 是一個很明顯的例子。一個線程安全的方法能夠在不一樣的線程中同時被調用。
在沒有並行訪問方法的狀況下,在其餘線程中調用對象的非線程安全的方法時可能運行了幾千次後纔會出現一個併發訪問,形成不可預料的行爲。編寫測試代碼並不能徹底的保證線程的正確性,但仍然很重要。在Linux中,Valgrind和Helgrind能夠偵測線程錯誤。
QThread 細節很是有意思:
· QThread 並不位於新線程 run()執行的位置中。它位於舊線程中。
· 大部分QThread 的方法是線程的控制接口中,並在舊線程中調用。不要使用moveToThread()將這些接口移到新建立的線程中,例如,調用moveToThread(this) 被認爲是一種壞的實踐。
· exec()和靜態方法usleep(), msleep(), sleep()應在新建立的線程中調用。
其餘的一些定義在 QThread 子類中的成員能夠在新舊線程中訪問。開發者負責協調這些訪問。 一種典型的策略是在調用 start() 前設置這些成員。一旦工做者線程運行起來,主線程不該當再修改這些成員。當工做者線程中止後,主線程又能夠訪問些額外的成員。這是一種在線程啓動前和中止後傳遞參數的方便的策略。
一個 QObject's 父類必須位於相同的線程中。對於run()方法中建立的對象,在這有一個很是驚人的結果。
使用一個互斥量 來保護數據的完整性
一個互斥量是一中且具備lock() 和 unlock() 方法的對象,並記住它是否被鎖住。互斥量可在多個線程中訪問。若是互斥量沒有被鎖定, lock() 會當即返回。下一個從其餘線程的調用會發現互斥量已經處於鎖定狀態,而後,lock() 會阻塞線程直到其餘線程調用 unlock()。該功能可保證一個代碼段在同一時間僅能被一個線程執行。
下面代碼顯示了怎樣使用一個互斥量來確保一個方法是線程安全的。
若是一個線程不能解鎖一個互斥量會發生什麼狀況呢?結果是應用程序會僵死。在上面的例子中,能夠會拋出異常且永遠不會到達mutex.unlock() 。爲了防止這種狀況,應該使用 QMutexLocker 。
這看上去很簡單,但互斥會引入新的問題:死鎖。當一個線程等待一個互斥量變爲解鎖,可是該互斥量仍然處於鎖定狀態,由於佔有該互斥量的線程在等待第一個線程解鎖該互斥量。結果是一個僵死的應用程序。互斥量用於保證一個方法是線程安全的。大部分Qt方法不是線程安全的,由於當使用互斥量時老是有些性能損失。
在一個方法中並不老是可以加鎖和解鎖一個互斥量。有時,鎖定的範圍跨越了數個調用。例如,利用迭代器修改一個容器時須要幾個調用組成的序列,這個序列不能被其餘線程中斷。在這種狀況下,利用外部鎖就能夠保證這個調用序列是被鎖定的。利用一個外部鎖,鎖定的時間能夠根據操做的須要進行調整。很差之處是外部鎖幫助鎖定,但不能強制執行它,由於對象的使用者可能忘記使用它。
使用事件循環來防止數據崩潰
Qt的事件循環對線程間通訊是一個很是有價值的工具。每一個線程能夠擁有本身的事件循環。調用另外一個線程中的槽的安全方法就是將此調用放在該線程的事件循環中。這確保了目標對象在啓動另外一方法前完成了當前正在執行的方法。那麼,怎樣將一個方法調用放到一個事件循環中呢?Qt有兩種方式。一種方式是經過queued信號-槽鏈接;另外一種方式就是利用QCoreApplication::postEvent()發送一個事件。一個queued 信號-槽鏈接是一種異步執行的信號槽鏈接。內部實現是基於發送的事件。信號的參數放置到事件循環中,信號方法會當即返回。
鏈接的槽執行的時間取決於事件循環中的基於事件。經過事件循環通訊消除了使用互斥量面臨的死鎖問題。這就是爲何咱們建議使用事件循環而不是使用互斥量鎖定一個對象。
處理異步執行
一種得到工做者線程結果的方式是等待該線程中止。然而,在許多狀況下,阻塞的等待是不可接受的。另外一種方式是經過發送的事件或queued信號和槽來得到異步結果。這產生了一些開銷,由於一個操做的結果並非出如今下一個代碼行,而是在一個位於其餘地方的槽中。Qt開發者習慣了這種異步行爲,由於它與GUI應用程序中事件驅動的方式很是相似。
例子
該手冊提供了一些例子,演示了在Qt中使用線程的三種基本方法。另外兩個例子演示了怎樣與一個運行中的線程進行通訊以及一個 QObject 可被置於另外一個線程中,爲主線程提供服務。
· 使用 QThread 使用如上所示。
· 使用全局的QThreadPool
· 使用 QtConcurrent
· 與GUI線程進行通訊
· 在另外一個線程的常駐對象爲主線程提供服務
以下 的例子能夠單獨地進行編譯和運行。源碼可在源碼目錄中找到:examples/tutorials/threads/
例 1: 使用Thread Pool
不停地建立和銷燬線程很是耗時,可使用一個線程池。線程池能夠存取線程和獲取線程。咱們可使用全局線程池寫一個與上面相同的"hello thread" 程序 。咱們從QRunnable派生出一個類。在另外一個線程中運行的代碼必須放在重載的QRunnable::run()方法中。
在main()中, 咱們實例化了Work, 定位於全局的線程池,使用QThreadPool::start()方法。如今,線程池在另外一個線程中運行咱們的工做。 使用線程池有一些性能上的優點,由於線程在它們結束後沒有被銷燬,它們被保留在線程池中,等待以後再次被使用。
例 2: 使用 QtConcurrent
咱們寫一個全局的函數hello()來實現工做者代碼。QtConcurrent::run()用於在另外一個線程中運行該函數。該結果是QFuture。 QFuture 提供了一個方法叫waitForFinished(), 它阻塞主線程直到計算完成。當所需的數據位於容器中時,QtConcurrent才顯示它真正的威力。 QtConcurrent 提供了一些函數能並行地處理這些已經成爲容器裏元素的一些數據。使用QtConcurrent很是相似於應用一個STL算法到某個STL容器類。QtConcurrent Map是一個很是簡短且清晰的例子,它演示了容器中的圖片怎麼被擴散到全部核中去處理。對於每一個阻塞函數,都同時存在一個非阻塞, 異步型函數。異步地獲取結果是經過QFuture 和QFutureWatcher來實現的。
例 3: Clock
咱們想建立一個時鐘應用程序。該應用程序有一個GUI和一個工做者線程。工做者線程每10毫秒檢查一下當前的時間。若是格式化的時間發生了變化,該結果會發送給顯示時間的GUI線程當中。
固然, 這是一種過分複雜的方式來設計一個時鐘,事實上,一個獨立的線程不必。使用計時器會更好。本例子純粹是用於教學目的的,演示了從工做者線程向GUI線程進行通訊。 注意,這種通訊方式很是容易,咱們僅須要添加一個信號給QThread, 而後構建一個queued 信號/槽鏈接到主線程中。從GUI到 工做者線程的方式在下一個例子中演示。
咱們已經將 clockThread 與標籤鏈接起來。鏈接必須是一個queued 信號-槽鏈接,由於咱們想將調用放到事件循環當中。
咱們從 QThread 派生出一個類,並聲明sendTime()信號。
該例子中最值得關注的部分是計時器經過一個直接鏈接與它的槽相連。默認的鏈接會產生一個queued 信號-槽鏈接,由於被鏈接的對象位於不一樣的線程。記住,QThread並不位於它建立的線程中。可是,從工做者線程中訪問ClockThread::timerHit() 仍然是安全的,由於ClockThread::timerHit()是私有的,且只處理私有變量。QDateTime::currentDateTime() 在Qt文檔中並未標記爲線程安全的,可是在此例子中,咱們能夠放心使用,由於咱們知道訪方法沒有會其餘的線程中使用。
例 4: A 常駐線程
該例子演示了位於工做者線程中的一個QObject接受來自GUI線程的請求,利用一個計時器進行輪詢,並不時地將結果返回給GUI線程。實現的工做包括輪詢必須實如今一個從QObject派生出的類中。在以下代碼中,咱們已稱該類爲 WorkerObject。 線程相關的代碼已經隱藏在稱爲Thread類中,派生自QThread. Thread有兩個額外的公共成員。launchWorker() 獲取工做者對象並將其移到另外一個開啓了事件循環的線程中。 該調用阻塞一小會,直到建立操做完成,使得工做者對象能夠在下一行被再次使用。Thread 類的代碼短但有點複雜,所以咱們只顯示怎樣使用該類。
QMetaObject::invokeMethod()經過事件循環調用槽。worker對象的方法不該該在對象被移動到另外一個線程中直接調用。咱們讓工做者線程執行一個工做和輪詢,並使用一個計時器在3秒後關閉該應用程序。關閉worker須要小心。咱們調用 Thread::stop() 退出事件循環。咱們等待線程中止,當線程中止後,咱們刪除worker。