Qt 線程基礎(QThread、QtConcurrent等)

昨晚看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程序中使用的的事件驅動編程。

原文:http://blog.csdn.net/dbzhang800/article/details/6554104

博文索引  持續更新中。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章