Qt 線程基礎(QThread、QtConcurrent、QThreadPool等)


 

使用線程

基本上有種使用線程的場合:

  • 通過利用處理器的多個核使處理速度更快。
  • 為保持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程序中使用的的事件驅動編程。

 

***************************************************************************************************

 

一、QThreadPool類

  QThreadPool管理一組線程。它負責管理和回收單個QThread對象以減少程序中線程創建的開銷。每個Qt應用程序都有一個全局的QThreadPool對象,可通過方法globalInstance()獲得。為了調用QThreadPool中的一個線程,需要提供一個從QRunnable繼承過來的類,並實現其中的run方法。然后創建一個該類的對象,傳遞給QThreadPool::start()方法。代碼片斷如下:

[cpp]   view plain  copy
  1. class HelloWorldTask : public QRunnable  
  2.  {  
  3.      void run()  
  4.      {  
  5.          qDebug() << "Hello world from thread" << QThread::currentThread();  
  6.      }  
  7.  }  
  8.   
  9.  HelloWorldTask *hello = new HelloWorldTask();  
  10.  // QThreadPool takes ownership and deletes 'hello' automatically  
  11.  QThreadPool::globalInstance()->start(hello);  

默認情況下, QThreadPool自動刪除QRunnable對象。使用QRunnable::setAutoDelete()方法可以改變該默認行為。QThreadPool支持在QRunnable::run方法中通過調用tryStart(this)來多次執行相同的QRunnable。當最后一個線程退出run函數后,如果autoDelete啟用的話,將刪除QRunnable對象。在autoDelete啟用的情況下,調用start()方法多次執行同一QRunnable會產生競態,就避免這樣做。

  那些在一定時間內會使用的線程將會過期。默認的過期時間是30秒。可通過setExpiryTimeout()方法來設置。設置一個負數的超時值代表禁用超時機制。方法maxThreadCount()可以查詢可使用的最大線程數,你也可以設置最大的線程數。activeThreadCount反應的是當前正在被使用中的線程數個數。reserveThread函數保留某個線程為外部使用,releaseThread釋放該線程,這樣就可以被再次使用。

二、QtConcurrent命名空間

QtConcurrent命名空間里提供了一些高級API,利用這些API可以編寫多線程程序,而不用直接使用比較低級的一些類,如mutext,lock, waitcondition以及semaphore等。使用QtConcurrent命令空間的API編寫的程序會根據處理器的數目自動地調整線程的個數。QtConcurrent包含了用於並行列表處理的函數式編程,包含實現共享內存系統的MapReduce和FilterReduce, 以及管理GUI應用程序中異步計算的類。相關的類說明如下:

QtConcurrent::map()

appliesa function to every item in a container, modifying the itemsin-place

QtConcurrent::mapped()

islike map(), except that it returns a new container with themodifications

QtConcurrent::mappedReduced()

islike mapped(), except that the modified results are reduced orfolded into a single result.

QtConcurrent::filter()

litems from a container based on the result of a filter function.

QtConcurrent::filtered()

islike filter(), except that it returns a new container with thefiltered results

QtConcurrent::filteredReduced()

islike filtered(), except that the filtered results are reduced orfolded into a single result

QtConcurrent::run() 

runsa function in another thread.

QFutureIterator

allowsiterating through results available via QFuture.

QFutureWatcher

allowsmonitoring a QFuture usingsignals-and-slots.

QFutureSynchronizer

isa convenience class that automatically synchronizes severalQFutures.

代碼實例:

[cpp]   view plain  copy
  1. using namespace QtConcurrent;  
  2. void hello(QString name)  
  3. {  
  4.     qDebug() << "Hello" << name << "from" << QThread::currentThread();  
  5. }  
  6. int main(int argc, char **argv)  
  7. {  
  8.     QApplication app(argc, argv);  
  9.     QFuture<void> f1 = run(hello, QString("Alice"));  
  10.     QFuture<void> f2 = run(hello, QString("Bob"));  
  11.     f1.waitForFinished();  
  12.     f2.waitForFinished();  
  13.     return app.exec();  

**********************************************************************************************************

 

 

QtConcurrent::run() 的使用

2011-11-25 10:36  155人閱讀   評論(0)   收藏   舉報

QFuture<T> run(const Class *object, T (Class::*fn)(Param1, Param2, Param3, Param4, Param5) const, const Arg1 &arg1,const Arg2 &arg2, const Arg3 &arg3, const Arg4 &arg4, const Arg5 &arg5)

run()函數的原型如上,此函數是QtConcurrent命名空間里的函數.主要功能是令啟動一個線程來執行一個函數.Concurrent的英文示意就是並發的意思.

下面簡要的介紹run()函數的使用方法:

1.首先要有一個需要在另外開啟的線程中執行的函數:

void thread_add(QObject *receiver,int a,int b)

{
 QString message=QString("%1 + %2 = %3").arg(a).arg(b).arg(a+b);
 QApplication::postEvent(receiver,new ProgressEvent(true, message));
}

函數在線程中運行完畢后會向receiver發送一個消息,來返回結果.

 

2.有了要在線程中運行的函數,再來看看怎么啟動線程來運行這個函數

void MainWindow::on_pushButton_clicked()

{
 
          
for(int i=0;i<9;i++)
 for(int j=0;j<9;j++)
 QtConcurrent::run(thread_add,this,i,j);
}

點擊一個按鈕就會運行這段代碼,然后啟動8*8=64個線程,線程要運行的函數就是thread_add(之前定義的),消息接收對象就是MainWindow這個類的實例

 

3.線程得到了運行會發送消息給MainWindow,MainWindow重新實現bool MainWindow::event ( QEvent * event )處理接收到的消息,並顯示出來

bool MainWindow::event ( QEvent * event )

{
 if (event->type() ==
 static_cast<QEvent::Type>(ProgressEvent::EventId)) {
 ProgressEvent *progressEvent =
 static_cast<ProgressEvent*>(event);
 Q_ASSERT(progressEvent);
 ui->teLog->append(progressEvent->message);
 return true;
 }
 return QMainWindow::event(event);
}

 

再給出自定義的消息結構

struct ProgressEvent : public QEvent

{
 enum {EventId = QEvent::User};
 explicit ProgressEvent(bool saved_, const QString &message_)
 : QEvent(static_cast<Type>(EventId)),
 saved(saved_), message(message_) {}
 const bool saved;
 const QString message;
};


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM