Qt 執行緒基礎(QThread、QtConcurrent等)

pamxy發表於2013-03-27

轉自:http://blog.csdn.net/dbzhang800/article/details/6554104

目錄(?)[+]

昨晚看Qt的Manual,突然發現下一個版本的Qt中(Qt4.7.4、Qt4.8等)增加了一個特讚的介紹多執行緒的文章 :

注意:

  • 該連結以後會失效,但是 到時候你直接看Qt自帶Manual就行了
  • 本文不是嚴格的翻譯 dbzhang800 2011.06.18

使用執行緒

基本上有種使用執行緒的場合:

  • 通過利用處理器的多個核使處理速度更快。
  • 為保持GUI執行緒或其他高實時性執行緒的響應,將耗時的操作或阻塞的呼叫移到其他執行緒。

何時使用其他技術替代執行緒

開發人員使用執行緒時需要非常小心。啟動執行緒是很容易的,但確保所有共享資料保持一致很難。遇到問題往往很難解決,這是由於在一段時間內它可能只出現一次或只在特定的硬體配置下出現。在建立執行緒來解決某些問題之前,應該考慮一些替代的技術 

替代技術

註解

QEventLoop::processEvents()

在一個耗時的計算操作中反覆呼叫QEventLoop::processEvents() 可以防止介面的假死。儘管如此,這個方案可伸縮性並不太好,因為該函式可能會被呼叫地過於頻繁或者不夠頻繁。

QTimer

後臺處理操作有時可以方便地使用Timer安排在一個在未來的某一時刻執行的槽中來完成。在沒有其他事件需要處理時,時間隔為0的定時器超時事件被相應

QSocketNotifier 
QNetworkAccessManager 
QIODevice::readyRead()

這是一個替代技術,替代有一個或多個執行緒在慢速網路執行阻塞讀的情況。只要響應部分的計算可以快速執行,這種設計比線上程中實現的同步等待更好。與執行緒相比這種設計更不容易出錯且更節能(energy efficient)。在許多情況下也有效能優勢。

一般情況下,建議只使用安全和經過測試的方案而避免引入特設執行緒的概念。QtConcurrent 提供了一個將任務分發到處理器所有的核的易用介面。執行緒程式碼完全被隱藏在 QtConcurrent 框架下,所以你不必考慮細節。儘管如此,QtConcurrent 不能用於執行緒執行時需要通訊的情況,而且它也不應該被用來處理阻塞操作。

應該使用 Qt 執行緒的哪種技術?

有時候,你需要的不僅僅是在另一執行緒的上下文中執行一個函式。您可能需要有一個生存在另一個執行緒中的物件來為GUI執行緒提供服務。也許你想在另一個始終執行的執行緒中來輪詢硬體埠並在有關注的事情發生時傳送訊號到GUI執行緒。Qt為開發多執行緒應用程式提供了多種不同的解決方案。解決方案的選擇依賴於新執行緒的目的以及執行緒的生命週期。

生命週期

開發任務

解決方案

一次呼叫

在另一個執行緒中執行一個函式,函式完成時退出執行緒

編寫函式,使用QtConcurrent::run 執行它

派生QRunnable,使用QThreadPool::globalInstance()->start() 執行它

派生QThread,重新實現QThread::run() ,使用QThread::start() 執行它

一次呼叫

需要操作一個容器中所有的項。使用處理器所有可用的核心。一個常見的例子是從影象列表生成縮圖。

QtConcurrent 提供了map()函你數來將操作應用到容器中的每一個元素,提供了fitler()函式來選擇容器元素,以及指定reduce函式作為選項來組合剩餘元素。

一次呼叫

一個耗時執行的操作需要放入另一個執行緒。在處理過程中,狀態資訊需要傳送會GUI執行緒。

使用QThread,重新實現run函式並根據需要傳送訊號。使用訊號槽的queued連線方式將訊號連線到GUI執行緒的槽函式。

持久執行

生存在另一個執行緒中的物件,根據要求需要執行不同的任務。這意味著工作執行緒需要雙向的通訊。

派生一個QObject物件並實現需要的訊號和槽,將物件移動到一個執行有事件迴圈的執行緒中並通過queued方式連線的訊號槽進行通訊。

持久執行

生存在另一個執行緒中的物件,執行諸如輪詢埠等重複的任務並與GUI執行緒通訊。

同上,但是在工作執行緒中使用一個定時器來輪詢。儘管如此,處理輪詢的最好的解決方案是徹底避免它。有時QSocketNotifer是一個替代。

Qt執行緒基礎

QThread是一個非常便利的跨平臺的對平臺原生執行緒的抽象。啟動一個執行緒是很簡單的。讓我們看一個簡短的程式碼:生成一個線上程內輸出"hello"並退出的執行緒。

 // hellothread/hellothread.h
 class HelloThread : public QThread
 {
     Q_OBJECT
 private:
     void run();
 };

我們從QThread派生出一個類,並重新實現run方法。

 // hellothread/hellothread.cpp
 void HelloThread::run()
 {
      qDebug() << "hello from worker thread " << thread()->currentThreadId();
 }

run方法中包含將在另一個執行緒中執行的程式碼。在本例中,一個包含執行緒ID的訊息被列印出來。 QThread::start() 將在另一個執行緒中被呼叫。

 int main(int argc, char *argv[])
 {
     QCoreApplication app(argc, argv);
     HelloThread thread;
     thread.start();
     qDebug() << "hello from GUI thread " << app.thread()->currentThreadId();
     thread.wait();  // do not exit before the thread is completed!
     return 0;
 }

QObject與執行緒

QObject有執行緒關聯(thread affinity)[如何翻譯?關聯?依附性?dbzhang800 20110618],換句話說,它生存於一個特定的執行緒。這意味著,在建立時QObject儲存了到當前執行緒的指標。當事件使用postEvent()被派發時,這個資訊變得很有用。事件被放置到相應執行緒的事件迴圈中。如果QObject所依附的執行緒沒有事件迴圈,該事件將永遠不會被傳遞。

要啟動事件迴圈,必須在run()內呼叫exec()。執行緒關聯可以通過moveToThread()來更改。

如上所述,當從其他執行緒呼叫物件的方法時開發人員必須始終保持謹慎。執行緒關聯不會改變這種狀況。 Qt文件中將一些方法標記為執行緒安全。postEvent()就是一個值得注意的例子。一個執行緒安全的方法可以同時在不同的執行緒被呼叫。

通常情況下並不會併發訪問的一些方法,在其他執行緒呼叫物件的非執行緒安全的方法在出現造成意想不到行為的併發訪問前數千次的訪問可能都是工作正常的。編寫測試程式碼不能完全確保執行緒的正確性,但它仍然是重要的。在Linux上,Valgrind和Helgrind有助於檢測執行緒錯誤。

QThread的內部結構非常有趣:

  • QThread並不生存於執行run()的新執行緒內。它生存於舊執行緒中。
  • QThread的大多數成員方法是執行緒的控制介面,並設計成從舊執行緒中被呼叫。不要使用moveToThread()將該介面移動到新建立的執行緒中;呼叫moveToThread(this)被視為不好的實踐。
  • exec()和靜態方法usleep()、msleep()、sleep()要在新建立的執行緒中呼叫。
  • QThread子類中定義的其他成員可在兩個執行緒中訪問。開發人員負責訪問的控制。一個典型的策略是在start()被呼叫前設定成員變數。一旦工作執行緒開始執行,主執行緒不應該操作其他成員。當工作執行緒終止後,主執行緒可以再次訪問其他成員。這是一個線上程開始前傳遞引數並在結束後收集結果的便捷的策略。

QObject必須始終和parent在同一個執行緒。對於在run()中生成的物件這兒有一個驚人的後果:

 void HelloThread::run()
 {
      QObject *object1 = new QObject(this);  //error, parent must be in the same thread
      QObject object2;  // OK
      QSharedPointer <QObject> object3(new QObject); // OK
 }

使用互斥量保護資料的完整

互斥量是一個擁有lock()和unlock()方法並記住它是否已被鎖定的物件。互斥量被設計為從多個執行緒呼叫。如果訊號量未被鎖定lock()將立即返回。下一次從另一個執行緒呼叫會發現該訊號量處於鎖定狀態,然後lock()會阻塞執行緒直到其他執行緒呼叫unlock()。此功能可以確保程式碼段將在同一時間只能由一個執行緒執行。

使用事件迴圈防止資料破壞

Qt的事件迴圈對執行緒間的通訊是一個非常有價值的工具。每個執行緒都可以有它自己的事件迴圈。在另一個執行緒中呼叫一個槽的一個安全的方法是將呼叫放置到另一個執行緒的事件迴圈中。這可以確保目標物件呼叫另一個的成員函式之前可以完成當前正在執行的成員函式。

那麼,如何才能把一個成員呼叫放於一個事件迴圈中? Qt的有兩種方法來做這個。一種方法是通過queued訊號槽連線;另一種是使用QCoreApplication::postEvent()派發一個事件。queued的訊號槽連線是非同步執行的訊號槽連線。內部實現是基於posted的事件。訊號的引數放入事件迴圈後訊號函式的呼叫將立即返回。

連線的槽函式何時被執行依賴於事件迴圈其他的其他操作。

通過事件迴圈通訊消除了我們使用互斥量時所面臨的死鎖問題。這就是我們為什麼推薦使用事件迴圈,而不是使用互斥量鎖定物件的原因。

處理非同步執行

一種獲得一個工作執行緒的結果的方法是等待執行緒終止。在許多情況下,一個阻塞等待是不可接受的。阻塞等待的替代方法是非同步的結果通過posted事件或者queued訊號槽進行傳遞。由於操作的結果不會出現在原始碼的下一行而是在位於原始檔其他部分的一個槽中,這會產生一定的開銷,因為,但在位於原始檔中其他地方的槽。 Qt開發人員習慣於使用這種非同步行為工作,因為它非常相似於GUI程式中使用的的事件驅動程式設計。


相關文章