日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

【转】【QT】 Threads, Events and QObjects

發(fā)布時間:2025/4/9 c/c++ 41 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【转】【QT】 Threads, Events and QObjects 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.
前言:?qt wiki?中這篇文章3月份再次更新,文章對 QThread 的用法,使用場景,有很好的論述,可以作為 Qt 多線程編程的使用指南,原文在這里,原作者?peppe?開的討論貼在這里

原文以姓名標識-相同方式分享 2.5 通用版發(fā)布

Creative Commons Attribution-ShareAlike 2.5 Generic

?

背景

在?#qt IRC channel?[irc.freenode.net]?中,討論最多的話題之一就是多線程。很多同學(xué)選擇了多線程并行編程,然后……呃,掉進了并行編程的無盡的陷阱中。

由于缺乏 Qt 多線程編程經(jīng)驗(尤其是結(jié)合Qt 信號槽機制的異步網(wǎng)絡(luò)編程)加上一些現(xiàn)有的其他語言(工具)的使用經(jīng)驗,導(dǎo)致在使用 Qt 時,一些同學(xué)有朝自己腳開槍的行為。Qt 的多線程支持是一把雙刃劍:雖然 Qt 的多線程支持使得多線程編程變得簡單,但同時也引入了一些其他特性(尤其是與 QObject 的交互),這些特性需要特別小心。

本文的目的不是教你如何使用多線程,加鎖、并行、擴展性,這不是本文的重點,而且這些問題已經(jīng)有非常多的討論,可以參考這里?[doc.qt.nokia.com]?的推薦。本文作為 Qt 多線程的指南,目的是幫助開發(fā)者避免常見的陷阱,開發(fā)出更健壯的程序。

知識背景

本文不是介紹多線程編程的文章,繼續(xù)閱讀下面的內(nèi)容你需要以下的知識背景:

  • C++ 基礎(chǔ) (強烈推薦,其他語言亦可)
  • Qt 基礎(chǔ):QObject,信號槽,事件處理
  • 什么是線程,以及一個線程和其他線程、進程和操作系統(tǒng)之間的關(guān)系
  • 在主流的操作系統(tǒng)上,如何啟動和停止一個線程,如何等待線程結(jié)束
  • 如何使用互斥量(mutex),信號量(semaphore),條件等待(wait condition)創(chuàng)建線程安全/可重入的函數(shù),結(jié)構(gòu)和類。

本文中使用 Qt 的名詞定義?[doc.qt.nokia.com]:

  • 可重入?如果多個線程同時訪問某個類的(多個)對象且一個對象同時只有一個線程訪問,是安全的,那么這個類是可重入的。如果多個線程同時調(diào)用一個函數(shù)且只訪問該線程可見的數(shù)據(jù),是安全的,那么這個函數(shù)是可重入的。換句話說,訪問這些對象/共享數(shù)據(jù)時,必須通過外部加鎖機制來實現(xiàn)串行訪問,保證安全。
  • 線程安全?如果多個線程同時訪問某個類的對象是安全的,那么這個類是線程安全的。如果多個線程同時調(diào)用一個函數(shù)(即使訪問了共享數(shù)據(jù))是安全的,那么這個函數(shù)時線程安全的。

?

事件和事件循環(huán)

作為一個事件驅(qū)動的系統(tǒng),事件和事件分發(fā)在 Qt 的架構(gòu)中扮演著核心角色。本文不會全面覆蓋這個主題;我們主要闡述和線程相關(guān)的一些概念(有關(guān) Qt 事件系統(tǒng)的文章,請看這里,還有這里)。

在 Qt 中,一個事件是一個對象,它表示一些有趣的事情發(fā)生了;信號和事件的主要區(qū)別在于,在我們的程序中事件的目標是確定的對象(這個對象決定如何處理該事件),但信號可以發(fā)到“任何地方”。從代碼級別來講,所有的事件對象都是?QEvent??[doc.qt.nokia.com]?的子類,所有繼承自 QObject 的類都可以重寫 QObject::event() 虛函數(shù),來作為事件的目標處理者。

事件即可以來自應(yīng)用程序內(nèi)部,也可以來自外部;例如:

  • QKeyEvent 和 QMouseEvent 對象代表鼠標、鍵盤的交互,這些事件來自于窗口管理器。
  • QTimerEvent 對象會在計時器超時的時候,發(fā)送給另一個 QObject,這些事件(通常)來自于操作系統(tǒng)。
  • QChildEvent 對象會在添加或刪除一個child時,發(fā)送給另一個 QObject,這些事件來自于你的程序中。

關(guān)于事件,有一個很重要的事情,那就是事件不會一產(chǎn)生就發(fā)送給需要處理這個事件的對象;而是放到事件隊列中,然后再發(fā)送。事件分發(fā)器會循環(huán)處理事件隊列,把每個在隊列中的事件分發(fā)給相應(yīng)的對象,因此這個又叫做事件循環(huán)。從概念上講,事件循環(huán)看起來是這樣的:

while (is_active) {while (!event_queue_is_empty)dispatch_next_event();wait_for_more_events(); }

在 Qt 的使用中,通過調(diào)用 QCoreApplication::exec() 進入 Qt 的主消息循環(huán);這個函數(shù)會阻塞,直到調(diào)用 QCoreApplication::exit() 或 QCoreApplication::quit(),結(jié)束消息循環(huán)。

函數(shù) "wait_for_more_events()" 會阻塞(不是忙等)直到有事件產(chǎn)生。稍加考慮,我們就會發(fā)現(xiàn),在這時事件一定是從外部產(chǎn)生的(事件分發(fā)器已經(jīng)結(jié)束并且也沒有新的事件在事件隊列中等待分發(fā))。因此,事件循環(huán)可以在以下幾種情況下被喚醒:

  • 窗口管理器(鍵盤/鼠標點擊,和窗口的交互,等)
  • 套接字(sockets)(數(shù)據(jù)可讀、可寫、有新連接,等)
  • 計時器(計時器超時)
  • 從其他線程發(fā)送來的事件(稍后討論)

在 Unix-like 系統(tǒng)中,窗口管理器的活動(例如 X11)是通過套接字(socket)(Unix Domain or TCP/IP)通知給應(yīng)用程序的,因為客戶端是通過套接字和 X Server 通信的。如果我們使用內(nèi)部的 socketpair(2) 來實現(xiàn)跨線程的消息發(fā)送,那么我們要做的就是通過某些活動喚醒消息循環(huán):

  • 套接字(socket)
  • 計時器

系統(tǒng)調(diào)用 select(2) 是這么工作的:它監(jiān)聽著一個活動描述符的集合,如果一段時間(可配置超時事件)內(nèi)都沒有活動那么它就會超時。Qt 所需要做的就是把 select 返回的結(jié)果轉(zhuǎn)化為一個 QEvent 對象(子類對象)然后把它放入事件隊列中。現(xiàn)在你應(yīng)該知道消息循環(huán)內(nèi)部事怎么回事兒了。

哪些東西需要事件循環(huán)?

下面不是完整的列表,不過稍微思考一下,你就能猜出那些類需要消息循環(huán)了。

  • Widget 繪圖(painting)和交互:當(dāng)接收到 QPaintEvent 對象時,函數(shù) QWidget::paintEvent() 會被調(diào)用,QPaintEvent 對象的產(chǎn)生,有可能是調(diào)用 QWidget::update() (應(yīng)用程序內(nèi)部調(diào)用) 函數(shù),或者來自窗口管理器(例如:把一個隱藏的窗口顯示出來)。其他類型的交互(鼠標、鍵盤,等)也是一樣的:這些事件都需要一個事件循環(huán)來分發(fā)事件。
  • 計時器:簡單說,當(dāng) select(2) 或類似的調(diào)用超時的時候,計時器超時事件被觸發(fā),因此你需要消息循換來處理這些調(diào)用。
  • 網(wǎng)絡(luò)通信:所有 low-level 的 Qt 網(wǎng)絡(luò)通信類(QTcpSocket, QUdpSocket, QTcpServer,等)都設(shè)計為異步的。當(dāng)調(diào)用 read() 函數(shù)時,它們僅僅返回當(dāng)前可用的數(shù)據(jù),當(dāng)調(diào)用 write() 函數(shù)時,它們會安排稍后再寫。僅僅當(dāng)程序返回消息循換的時候,讀/寫操作才真正發(fā)生。注意雖然提供有同步的方法(那些以 waitFor* 命名的函數(shù)),但是它們并不好用,因為在等待的同時他們阻塞了消息循換。像 QNetworkAccessManager 這樣的 high-level 類,同樣需要消息循換,但不提供任何同步調(diào)用的接口。

阻塞消息循換

在討論為什么我們不應(yīng)該阻塞消息循換之前,先說明一下“阻塞”的含義是什么。想像一下,有一個在點擊時可以發(fā)送信號的按鈕,信號綁定到我們的工作類對象的一個槽函數(shù)上,這個槽函數(shù)會做很多工作。當(dāng)你點擊按鈕時,函數(shù)調(diào)用棧看起來應(yīng)該像下面這樣(棧底在上):

main(int, char **) QApplication::exec() […] QWidget::event(QEvent *) Button::mousePressEvent(QMouseEvent *) Button::clicked() […] Worker::doWork()

在 main() 函數(shù)中,我們通過調(diào)用 QApplication::exec() (第2行) 啟動了一個消息循換。窗口管理器發(fā)送一個鼠標點擊的事件,Qt 內(nèi)核會得到這個消息,然后轉(zhuǎn)化為一個 QMouseEvent 對象,通過 QApplication::notify()(此處沒有列出)函數(shù)發(fā)送給 widget 的 event() 函數(shù)(第4行)。如果按鈕沒有重寫 event() 函數(shù),那么他的基類(QWidget)實現(xiàn)的 event() 函數(shù)會被調(diào)用。QWidget::event() 檢測到鼠標點擊事件,然后調(diào)用相應(yīng)的事件處理函數(shù),就是上面代碼中的 Button::mousePressEvent()(第5行)函數(shù)。我們重寫了這個函數(shù),讓他發(fā)送一個 Button::clicked() 信號(第6行),這個信號會調(diào)用 Worker 類對象的槽函數(shù) Worker::doWork() (第8行)。

當(dāng) Worker 對象正在忙于工作的時候,消息循換在做什么?我們可能會猜測:什么也不做!消息循換分發(fā)了鼠標點擊事件然后等待,等待消息處理者返回。我們阻塞了消息循換,這意味在槽函數(shù) doWork() 返回之前,不會再有消息被分發(fā)出去,消息會不斷進入消息隊列而不能的得到及時的處理。

當(dāng)事件分發(fā)被卡住的時候,窗口不會刷新(QPaintEvent 對象在消息隊列中),不能響應(yīng)其他的交互行為(和前面的原因一樣),定時器超時事件不會觸發(fā)網(wǎng)絡(luò)通信變慢然后停止。此外,很多窗口管理器會檢測到你的程序不再處理事件,而提示程序無響應(yīng)。這就是為什么迅速的處理事件然后返回消息循環(huán)如此重要的原因。

強制分發(fā)事件

那么,如果有一個耗時的任務(wù)同時我們又不想阻塞消息循換,這時該如何去做?一個可能的回答是:把這個耗時的任務(wù)移動到其他的線程中:下一節(jié)中我們可以看到如何做。我們還有一個可選的辦法,那就是在我們耗時的任務(wù)中通過調(diào)用 QCoreApplication::processEvents() 來手動強制跑起消息循換。QCoreApplication::processEvents() 會處理所有隊列上的事件然后返回。

另一個可選的方案,我們可以利用?QEventLoop?[doc.qt.nokia.com]?強制再加入一個消息循環(huán)。通過調(diào)用 QEventLoop::exec() 函數(shù),我們加入一個消息循換,然后連接一個信號到? QEventLoop::quit() 槽函數(shù)上,來讓循環(huán)退出。例如:

QNetworkAccessManager qnam; QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...))); QEventLoop loop; QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit())); loop.exec(); /* reply has finished, use it */

QNetworkReply 不提供阻塞的接口,同時需要一個消息循環(huán)。我們進入了一個局部的 QEventLoop,當(dāng) reply 發(fā)出 finished 信號時,這個事件循環(huán)就結(jié)束了。

通過“其他路徑”重入消息循換時需要特別小心:這可能導(dǎo)致不期望的遞歸!回到剛才的按鈕例子中。如果我們再槽函數(shù) doWork() 中調(diào)用 QCoreApplication::processEvents() ,同時用戶再次點擊了按鈕,這個槽函數(shù) doWork() 會再一次被調(diào)用:

main(int, char **) QApplication::exec() […] QWidget::event(QEvent *) Button::mousePressEvent(QMouseEvent *) Button::clicked() […] Worker::doWork() // first, inner invocation QCoreApplication::processEvents() // we manually dispatch events and… […] QWidget::event(QEvent * ) // another mouse click is sent to the Button… Button::mousePressEvent(QMouseEvent *) Button::clicked() // which emits clicked() again… […] Worker::doWork() // DANG! we’ve recursed into our slot.

一個快速簡單的規(guī)避辦法是給 QCoreApplication::processEvents() 傳入一個參數(shù) QEventLoop::ExcludeUserInputEvents,它會告訴消息循換不要分發(fā)任何用戶輸入的事件(這些事件會停留在隊列中)。

幸運的是,同樣的問題不會出現(xiàn)在刪除事件中(調(diào)用 QObject::deleteLater() 會發(fā)送該事件到事件隊列中)。事實上,Qt 使用了特別的辦法來處理它,當(dāng)消息循環(huán)比 deleteLater 調(diào)用發(fā)生的消息循環(huán)更外層時,刪除事件才會被處理。例如:

QObject *object = new QObject; object->deleteLater(); QDialog dialog; dialog.exec();

這不會導(dǎo)致 object 空懸指針(QDialog::exec() 中的消息循環(huán),比 deleteLater 調(diào)用發(fā)生的地方層次更深)。同樣的事情也會發(fā)生在 QEventLoop 啟動的消息循環(huán)中。我只發(fā)現(xiàn)過一個例外(在 Qt 4.7.3 中),如果在沒有任何消息循環(huán)的時候調(diào)用了 deleteLater,那么第一個啟動的消息循環(huán)會處理這個消息,刪除該對象。這是很合理的,因為 Qt 知道不會有任何會執(zhí)行刪除動作的“外層”循環(huán),因此會立即刪除該對象。

?

Qt 線程類

Qt 支持多線程已經(jīng)很多年(2000 年9月22日發(fā)布的 Qt 2.2 引入了 QThread 類),4.0 版本在所有平臺上都默認開啟多線程支持(多線程支持是可以關(guān)閉的,更多細節(jié)看這里[doc.qt.nokia.com])。Qt 現(xiàn)在提供了很多類來實現(xiàn)多線程;下面就來看一下。

QThread

QThread?[doc.qt.nokia.com]?是 Qt 中多線程支持的核心的 low-level 類。一個 QThread 對象表示一個執(zhí)行的線程。由于 Qt 的跨平臺特性,QThread 設(shè)法隱藏了不同操作系統(tǒng)在線程操作中的所有平臺相關(guān)的代碼。

為了使用 Qthread 在一個線程中執(zhí)行代碼,我們繼承 QThread 然后重寫 QThread::run() 函數(shù):

class Thread : public QThread { protected:void run() {/* your thread implementation goes here */} };

然后這么使用

Thread *t = new Thread; t->start(); // start(), not run()!

來啟動一個新的線程。注意,從 Qt 4.4 開始,QThread 不再是抽象類,現(xiàn)在虛函數(shù) QThread::run() 有了調(diào)用 QThread::exec() 的默認實現(xiàn);它會啟動線程自己的消息循環(huán)(稍后詳細說明)。

QRunnable 和 QThreadPool

QRunnable?[doc.qt.nokia.com]?是一個輕量級的抽象類,它可以在另一個線程中啟動一個任務(wù),適用于“運行完就丟掉”這種情況。實現(xiàn)這個功能,我們需要做的就是繼承 QRunnable 然后實現(xiàn)純虛函數(shù) run():

class Task : public QRunnable { public:void run() {/* your runnable implementation goes here */} };

我們使用?QThreadPool?[doc.qt.nokia.com]?類,它管理著一個線程池,來真正運行一個 QRunnable 對象。當(dāng)調(diào)用 QThreadPool::start(runnable) 時,我們將 QRunnable 對象放入 QThreadPool 的執(zhí)行隊列中;當(dāng)線程可用時,QRunnable 對像會啟動,然后在線程中執(zhí)行。所有的 Qt 應(yīng)用程序都有一個全局的線程池,可以通過調(diào)用? QThreadPool::globalInstance() 來獲得,但是也可以創(chuàng)建一個私有的 QThreadPool 對象來顯式的管理。

注意,QRunnable 不是一個 QObject,因此沒有QObject內(nèi)建的和其他一些組建通信的機制;你不得不使用 low-level 線程原語手工處理(例如用互斥量保護隊列來收集結(jié)果等)。

QtConcurrent

QtConcurrent?[doc.qt.nokia.com]?是 high-level API,在 QThreadPool 基礎(chǔ)上構(gòu)建而成,它可以應(yīng)用在大部分常用的并行計算范式中:map?[en.wikipedia.org]),?reduce?[en.wikipedia.org]), 和filter?[en.wikipedia.org]);它同時提供 QtConcurrent::run() 方法,可以簡單的在另一個線程中啟動一個函數(shù)。

與 QThread 和 QRunnable 不同,QtConcurrent 不需要我們使用 low-level 的同步原語:所有 QtConcurrent 函數(shù)返回一個?QFuture?[doc.qt.nokia.com]?對象,它可以用來查詢計算狀態(tài)(進展),暫停/恢復(fù)/取消計算,同時它也包含計算的結(jié)果。QFutureWatcher?[doc.qt.nokia.com]?類可以用來監(jiān)測 QFuture 的進展,也可以通過信號槽來和 QFuture 交互(注意,QFuture 作為一個值語義的類,沒有繼承自 QObject)。

特性對比

\QThreadQRunnableQtConcurrent1
high level 接口nny
面向任務(wù)nyy
內(nèi)建支持暫停/恢復(fù)/取消nny
支持優(yōu)先級ynn
可以運行消息循環(huán)ynn
????

1?QtConcurrent::run 是個例外,因為它是使用 QRunnable 實現(xiàn)的,所以帶有 QRunnable 的特性。

?

線程和QObject

每個線程一個消息循環(huán)

到現(xiàn)在為止,我們已經(jīng)討論過“消息循環(huán)”,但討論的僅僅是在一個 Qt 應(yīng)用程序中只有一個消息循換的情況。但不是下面這種情況:QThread 對象可以啟動一個自己代表的線程中的消息循換。因此,我們把在 main() 函數(shù)中通過調(diào)用 QCoreApplication::exec()(該函數(shù)只能在主線程中調(diào)用)啟動的消息循換叫做主消息循環(huán)。它也叫做?GUI 線程,因為 UI 相關(guān)的操作只能(應(yīng)該)在該線程中執(zhí)行。一個 QThread 局部消息循換可以通過調(diào)用 QThread::exec() 來啟動(在 run() 函數(shù)中):

class Thread : public QThread { protected:void run() {/* ... initialize ... */exec();} };

上面我們提到,從 Qt 4.4 開始,QThread::run() 不再是一個純虛函數(shù),而是默認調(diào)用 QThread::exec()。和 QCoreApplication 一樣,QThread 也有 QThread::quit() 和 QThread::exit() 函數(shù),來停止消息循換。

一個線程的消息循環(huán)為所有在這個線程中的 QObject 對象分發(fā)消息;默認的,它包括所有在這個線程中創(chuàng)建的對象,或者從其他線程中移過來的對象(接下來詳細說明)。同時,一個 QObject 對象的線程相關(guān)性是確定的,也就是說這個對象生存在這個線程中。這個適用于在 QThread 對象的構(gòu)造函數(shù)中創(chuàng)建的對象:

class MyThread : public QThread { public:MyThread(){otherObj = new QObject;} private:QObject obj;QObject *otherObj;QScopedPointer<QObject> yetAnotherObj; };

在創(chuàng)建一個 MyThread 對象之后,obj,otherObj,yetAnotherObj 的線程相關(guān)性如何?我們必須看看創(chuàng)建這些對象的線程:它是運行 MyThread 構(gòu)造函數(shù)的線程。因此,所有這三個對象都不屬于 MyThread 線程,而是創(chuàng)建了 MyThread 對象的線程(MyThread 對象也屬于該線程)。

我們可以使用線程安全的 QCoreApplication::postEvent() 函數(shù)來給對象發(fā)送事件。它會把事件放入該對象所在消息循環(huán)的事件隊列中;因此,只有這個線程有消息循環(huán),消息才會被分發(fā)。

理解 QObject 和它的子類不是線程安全的(雖然它是可重入的)這非常重要;由于它不是線程安全的,所以你不能同時在多個線程中同時訪問同一個 QObject 對象,除非你自己串行化了所有對這些內(nèi)部數(shù)據(jù)的訪問(比如使用了互斥量來保護內(nèi)部數(shù)據(jù))。記住當(dāng)你從其他線程訪問 QObject 對象時,這個對象有可能正在處理它所在的消息循環(huán)分發(fā)給它的事件。同樣的,你也不能從另一個線程中刪除一個 QObject 對象,而必須使用 QObject::deleteLater() 函數(shù),它會發(fā)送一個事件到對象所在線程中,然后在該線程中刪除對象。

此外,QWidget 和它的所有子類,還有其他的 UI 相關(guān)類(非 QObject 子類,比如 QPixmap)還是不可重入的:他們僅僅可以在 UI 線程中使用。

我們可以通過調(diào)用 QObject::moveToThread() 來改變 QObject 對象和線程之前的關(guān)系,它會改變對象本身以及它的孩子與線程之前的關(guān)系。由于 QObject 不是線程安全的,所以我們必須在它所在的線程中使用;也就是說,你僅僅可以在他們所處的線程中把它移動到另一個線程,而不能從其他線程中把它從所在的線程中移動過。而且,Qt 要求一個 QObject 對象的漢子必須和他的父親在同一個線程中,也就是說:

  • 如果一個對象有父親,那么你不能使用 QObject::moveToThread() 把它移動到其他線程
  • 你不能在 QThread 類中以 QThread 為父親創(chuàng)建對象
class Thread : public QThread {void run() {QObject *obj = new QObject(this); // WRONG!!!} };

這是因為 QThread 對象所在的線程是另外的線程,即,QThread 對象所在的線程是創(chuàng)建它的線程。

Qt 要求所有在線程中的對象必須在線程結(jié)束之前銷毀;利用 QThread::run() 函數(shù),在該函數(shù)中僅創(chuàng)建棧上的對象,這一點可以很容易的做到。

跨線程信號槽

有了這些前提,我們?nèi)绾握{(diào)用另一個線程中 QObject 對象的函數(shù)?Qt 提供了一個非常漂亮和干凈的解決方案:我們發(fā)送一個事件到線程的消息隊列中,事件的處理,將調(diào)用我們感興趣的函數(shù)(當(dāng)然這個線程需要啟動一個事件循環(huán))。該設(shè)施圍繞 Qt 的元對象編譯器(MOC)提供的方法內(nèi)省而構(gòu)建:因此,信號,槽,函數(shù),只要使用了 Q_INVOKABLE 宏,那么就可以從另外的線程調(diào)用它。

QMetaObject::invokeMethod() 靜態(tài)方法為我們實現(xiàn)了這個功能:

QMetaObject::invokeMethod(object, "methodName",Qt::QueuedConnection,Q_ARG(type1, arg1),Q_ARG(type2, arg2));

注意,由于參數(shù)需要在消息傳遞時拷貝,這些類型的參數(shù)需要提供公有的構(gòu)造函數(shù),析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù),而且要使用 qRegisterMetaType() 函數(shù)將類型注冊到 Qt 類型系統(tǒng)中。

跨線程的信號槽工作方式是類似的。當(dāng)我們將信號和曹連接時,QObject::connect 函數(shù)的第5個參數(shù)可以指定連接的類型:

  • direct connection:意思是槽函數(shù)會在信號發(fā)送的線程中直接被調(diào)用。
  • queued connection:意思是事件會發(fā)送到接收者所在線程的消息隊列中,消息循環(huán)會稍后處理該事件然后調(diào)用槽函數(shù)。
  • blocking queued connection:和 queued connection 類似,但是發(fā)送線程會阻塞,直到接收者所在線程的消息循環(huán)處理了該事件,調(diào)用了槽函數(shù)之后,才會返回;

在任何情況下,記住發(fā)送者所在的線程一點都不重要!在自動連接的情況下,Qt 會檢查信號調(diào)用的線程,然后與接收者所在線程比較,然后決定使用哪種連接類型。特別的,Threads and QObjects?[doc.qt.nokia.com]?(4.7.1) 在下面的情況下是錯誤的

自動連接(默認值),如果發(fā)送者和接收者在同一線程它和直接連接(direct connection)的行為是一樣的;如果發(fā)送者和接收者在不同的線程它和隊列連接(queued connection)的行為是一樣的。

因為發(fā)送者所在的線程和無關(guān)緊要的。例如:

class Thread : public QThread {Q_OBJECTsignals:void aSignal();protected:void run() {emit aSignal();} };/* ... */ Thread thread; Object obj; QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot())); thread.start();

信號 aSignal() 會在一個新的線程中發(fā)送(Thread 對象創(chuàng)建的線程);因為這不是 Object? 對象所在的線程(但這時,Object 對象與 Thread 對象在同一個線程中,再次強調(diào),發(fā)送者所在線程是無關(guān)緊要的),這時將使用 queued connection。

另一個常見的陷阱:

class Thread : public QThread {Q_OBJECTslots:void aSlot() {/* ... */}protected:void run() {/* ... */} };/* ... */ Thread thread; Object obj; QObject::connect(&obj, SIGNAL(aSignal()), &thread, SLOT(aSlot())); thread.start(); obj.emitSignal();

當(dāng)“obj” 發(fā)送 aSignal() 信號時,將會使用哪種連接類型?你應(yīng)該已經(jīng)猜到了:direct connection。這是因為 Thread 對象所在線程就是信號發(fā)送的線程。在槽函數(shù) aSlot() 中,我們可能訪問 Thread 類的成員,而同時 run() 函數(shù)可能也在訪問,他們會同時進行:這是完美的災(zāi)難配方。

另一個例子,或許也是最重要的一個:

class Thread : public QThread {Q_OBJECTslots:void aSlot() {/* ... */}protected:void run() {QObject *obj = new Object;connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));/* ... */} };

在上面的情形中,連接類型是 queued connection,因此你需要在 Thread 對象所在線程啟動一個消息循環(huán)。

下面是一個你經(jīng)常可以在論壇、博客或其他地方看到的解決方案。那就是在 Thread 的構(gòu)造函數(shù)中增加一個 moveToThread(this) 函數(shù):

class Thread : public QThread {Q_OBJECT public:Thread() {moveToThread(this); // WRONG}/* ... */ };

這確實可以工作(因為現(xiàn)在線程對象所在的線程的確改變了),但是這是個非常糟糕的設(shè)計。錯誤在于我們誤解了 thread 對象(QThread 子類)的目的:QThread 對象不是線程本身;它是用于管理線程的,因此它應(yīng)該在另一個線程中使用(通常就是創(chuàng)建它的線程)。

一個好的辦法是:把“工作”部分從“控制”部分分離出來,創(chuàng)建 QObject 子類對象,然后使用 QObject::moveToThread() 來改變對象所在的線程:

class Worker : public QObject {Q_OBJECTpublic slots:void doWork() {/* ... */} };/* ... */ QThread *thread = new QThread; Worker *worker = new Worker; connect(obj, SIGNAL(workReady()), worker, SLOT(doWork())); worker->moveToThread(thread); thread->start();

應(yīng)該做&不應(yīng)該做

你可以…

  • 在 QThread 子類中添加信號。這是很安全的,而且可以“正確工作”(前面提到;發(fā)送者所在線程是無關(guān)緊要的)。

你不應(yīng)該…

  • 使用 moveToThread(this)
  • 強制連接類型:這通常說明你在做一些錯誤的事情,例如混合了 QThread 控制接口和程序邏輯(它應(yīng)該在該線程創(chuàng)建的對象中)
  • 在 QThread 子類中增加槽函數(shù):它們會在“錯誤的”線程中被調(diào)用,不是在 QThread 管理的線程中,而是在 QThread 對象創(chuàng)建的線程,迫使你使用 direct connection 或使用 moveToThread(this) 函數(shù)。
  • 使用 QThread::terminate 函數(shù)。

禁止…

  • 在線程還在運行時退出程序。使用 QThread::wait 等待線程終止。
  • 當(dāng) QThread 管理的線程還在運行時,刪除 QThread 對象。如果你想要“自動析構(gòu)”,你可以將 finished() 信號連接到 deleteLater() 槽函數(shù)上。

?

什么時候應(yīng)該使用線程?

當(dāng)使用阻塞 API 時

如果你需要使用沒有提供非阻塞API的庫(例如信號槽,事件,回調(diào)函數(shù),等),那么避免阻塞消息循環(huán)的唯一解決方案就是開啟一個進程或線程。由于創(chuàng)建一個工作進程,讓它完成任務(wù)并通過進程通信返回結(jié)果與開啟一個線程相比是困難并且昂貴的,所以創(chuàng)建一個線程是更普遍的做法。

地址解析(只是舉個例子,不是在討論蹩腳的第三方 API。這是每一個 C 語言函數(shù)庫中包含的東西)就是一個很好的例子,它把主機名轉(zhuǎn)換為地址。它會調(diào)用域名解析系統(tǒng)(DNS)來查詢。雖然一般情況下,它會立即返回,但是遠程服務(wù)器有可能故障,有可能丟包,有可能網(wǎng)絡(luò)突然中斷,等等。簡而言之,它可能需要等待很長時間才相應(yīng)我們發(fā)出的請求。

UNIX 系統(tǒng)中的標準 API 是阻塞的(不僅僅是舊的 API gethostbyname(3),新的更好的 getservbyname(3) 和 getaddrinfo(3) 也是一樣)。QHostInfo?[doc.qt.nokia.com]?是處理主機名解析的 Qt 類,它使用 QThreadPool 來使得請求在后臺運行(看這里?[qt.gitorious.com];如果線程支持被關(guān)閉的話,它會切換為阻塞方式)。

另一個簡單的例子是圖像加載和縮放。QImageReader?[doc.qt.nokia.com]?和?QImage?[doc.qt.nokia.com]?只提供阻塞方法來從設(shè)備讀取圖像,或改變圖像的分辨率。如果你正在處理非常大的圖像,這些操作可能會花費數(shù)十秒。

當(dāng)你想要充分利用多CPU時

多線程可以讓你的程序更好的利用多處理器系統(tǒng)。每個線程是由操作系統(tǒng)獨立調(diào)用的,如果你的程序運行在這樣的機器上,線程調(diào)度就可以讓多個處理器同時運行不同的線程。

比如,考慮一個批量生成縮略圖的程序。一個有 n 個線程的線程農(nóng)場(有固定線程數(shù)目的線程池),n 是系統(tǒng)中可用 CPU 的數(shù)量(可參考 QThread::idealThreadCount()),它可以將處理任務(wù)分布到多個cpu上,這樣我們就可以獲得與cpu數(shù)量有關(guān)的效率線性增長(簡單的,我們把CPU考慮為瓶頸)。

當(dāng)你不想被阻塞時

呃…從一個例子開始會更好。

這是一個高級話題,你可以暫時忽略。Webkit 中的 QNetworkAccessManager 是一個很好的例子。Webkit 是一個流行的瀏覽器引擎,它是處理網(wǎng)頁布局和顯式的一組類的集合,Qt 中 QwebView 類使用了它。

QNetworkAccessManager 是 Qt 中處理 HTTP 請求和響應(yīng)的類,我們可以把它當(dāng)作瀏覽器的引擎。Qt 4.8 之前,它沒有使用任何工作線程;所有的處理都在 QNetworkAccessManager 和 QNetworkReply 所在的同一個線程。

雖然在網(wǎng)絡(luò)通信中使用線程是一個好辦法,但是它也存在問題:如果你沒有盡快從 socket 中讀取數(shù)據(jù),內(nèi)核緩沖會被其他數(shù)據(jù)填充,數(shù)據(jù)包將被丟掉,可想而知,數(shù)據(jù)傳輸速率將下降。

socket 活動(也就是 socket 是否可讀)是由 Qt 的事件循環(huán)還管理的。阻塞事件循環(huán)會導(dǎo)致傳輸性能下降,因為這時沒有人會被告知現(xiàn)在數(shù)據(jù)已經(jīng)可讀(所以沒有人會去讀取數(shù)據(jù))。

但是什么會阻塞消息循環(huán)?可悲的是:WebKit 自己阻塞了消息循環(huán)。一旦消息可讀,Webkit 開始處理網(wǎng)頁布局。不幸的是,這個處理是復(fù)雜而昂貴的,它會阻塞消息循換一(小)會兒,但足以影響傳輸效率(寬帶連接這里起到了作用,在短短幾秒內(nèi)就可填滿內(nèi)核緩存)。

總結(jié)一下,這個過程發(fā)生的事情:

  • Webkit 發(fā)起請求;
  • 一些響應(yīng)數(shù)據(jù)開始到達;
  • Webkit 開始使用到達的數(shù)據(jù)來網(wǎng)頁布局,阻塞了事件循環(huán);
  • 沒有了事件循環(huán),操作系統(tǒng)接收到了數(shù)據(jù),但沒有人從 QNetworkAccessManager 的 socket 中讀取數(shù)據(jù);
  • 內(nèi)核緩沖將被其他數(shù)據(jù)填充,從而導(dǎo)致傳輸效率下降。

整個頁面的加載時間由于 Webkit 自己引起的問題而變得很慢。

注意,由于 QNetworkAccessManager 和 QNetworkReply 都是 QObject,它們都不是線程安全的,因此你不能將它移動到另一個線程然后繼續(xù)在你的線程中繼續(xù)使用它,因為你可能從兩個線程中同時訪問它:你自己的線程和它所在的線程,因為它所在的消息循環(huán)會將事件分發(fā)給它處理。

在 Qt 4.8 中,QNetworkAccessManager 現(xiàn)在默認使用單獨的線程處理 HTTP 請求,因此 UI 反應(yīng)慢和系統(tǒng)緩沖被填充過快的問題得以解決。

?

什么時候不應(yīng)該使用線程?

計時器

這可能是最糟糕的線程濫用。如果你不得不重復(fù)調(diào)用一個方法(例如,每秒調(diào)用一次),很多人會這么做:

// VERY WRONG while (condition) {doWork();sleep(1); // this is sleep(3) from the C library }

然后會發(fā)現(xiàn)這阻塞了事件循環(huán),然后決定使用線程來解決:

// WRONG class Thread : public QThread { protected:void run() {while (condition) {// notice that "condition" may also need volatiness and mutex protection// if we modify it from other threads (!)doWork();sleep(1); // this is QThread::sleep()}} };

一個更好更簡單的辦法是使用計時器,一個超時時間為1秒的?QTimer?[doc.qt.nokia.com]?對象,和 doWork() 槽函數(shù):

class Worker : public QObject {Q_OBJECTpublic:Worker() {connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));timer.start(1000);}private slots:void doWork() {/* ... */}private:QTimer timer; };

我們所需要做的就是啟動一個消息循環(huán),然后 doWork() 函數(shù)會每一秒調(diào)用一次。

網(wǎng)絡(luò)通信/狀態(tài)機

下面是一個非常常見的網(wǎng)絡(luò)通信的設(shè)計:

socket->connect(host); socket->waitForConnected();data = getData(); socket->write(data); socket->waitForBytesWritten();socket->waitForReadyRead(); socket->read(response);reply = process(response);socket->write(reply); socket->waitForBytesWritten(); /* ... and so on ... */

不用多說,這些 waitFor*() 函數(shù)調(diào)用會阻塞消息循環(huán),凍結(jié) UI,等等。注意,上面的代碼沒有任何的錯誤處理,不然它會更繁瑣。上面的錯誤在于我們忘記了最初網(wǎng)絡(luò)設(shè)計的就是異步的,如果我們使用同步處理,那就是朝自己的腳開槍。解決上面的問題,許多人會簡單的把它移動到不同的線程中。

另一個更抽象的例子:

result = process_one_thing();if (result->something())process_this(); elseprocess_that();wait_for_user_input(); input = read_user_input(); process_user_input(input); /* ... */

它和上面網(wǎng)絡(luò)的例子有著同樣的陷阱。

讓我們退一步,從更高的視角來看看我們構(gòu)建的東西,我們構(gòu)建了一個狀態(tài)機來處理輸入。

  • 空閑 –> 連接中(調(diào)用 connectToHost())
  • 連接中 –> 已連接 (發(fā)出 connected() 信號)
  • 已連接 –> 發(fā)送登陸數(shù)據(jù)(發(fā)送登陸數(shù)據(jù)到服務(wù)器)
  • 發(fā)送登陸數(shù)據(jù) –> 登陸成功(服務(wù)器返回 ACK)
  • 發(fā)送登陸數(shù)據(jù) –> 登陸失敗(服務(wù)器返回 NACK)

等等。

現(xiàn)在,我們有很多辦法來構(gòu)建一個狀態(tài)機(Qt 就為我們提供了一個可使用的類:QStateMachine?[doc.qt.nokia.com]),最簡單的辦法就是使用枚舉(整型)來記錄當(dāng)前的狀態(tài)。我們可以重寫上面的代碼:

class Object : public QObject {Q_OBJECTenum State {State1, State2, State3 /* and so on */};State state;public:Object() : state(State1){connect(source, SIGNAL(ready()), this, SLOT(doWork()));}private slots:void doWork() {switch (state) {case State1:/* ... */state = State2;break;case State2:/* ... */state = State3;break;/* etc. */}} };

“source” 對象和“ready()”信號是什么?我們想要的是:拿網(wǎng)絡(luò)例子來說,我們想要把 QAbstractSocket::connected() 和 QIODevice::readyRead() 連接到我們的槽函數(shù)上。當(dāng)然,如果再多些槽函數(shù)更好的話,我們也可以增加更多(比如錯誤處理的槽函數(shù),由 QAbstractSocket::error() 信號來發(fā)起)。這是真正的異步,信號驅(qū)動的設(shè)計!

把任務(wù)分解成小塊

想想一下我們有個很耗時但是無法移動到其它線程的任務(wù)(或者根本不能移動到其它線程,因為它可能必須在 UI 線程中執(zhí)行)。如果我們把任務(wù)分解成小塊,那么我們就可以返回消息循環(huán),讓消息循環(huán)分發(fā)事件,然后讓它調(diào)用處理后續(xù)任務(wù)塊的函數(shù)。如果我們還記得 queued connection 如何實現(xiàn)的話,那就很容易解決這個問題了:事件發(fā)送到接收者所在的事件循環(huán)中,當(dāng)事件被分發(fā)的時候,相應(yīng)的槽函數(shù)被調(diào)用。

我們可以使用 QMetaObject::invokeMethod() 函數(shù),用參數(shù) Qt::QueuedConnection 指定連接類型,來實現(xiàn)這個功能;這需要函數(shù)可調(diào)用,也就是說函數(shù)必須是個槽函數(shù)或者使用了 Q_INVOKABLE 宏修飾。如果我們還要給函數(shù)傳遞參數(shù),那么我們要保證參數(shù)類型已經(jīng)通過函數(shù) qRegisterMetaType() 注冊到了 Qt 的類型系統(tǒng)中。下面的代碼給我們展示了這種做法:

class Worker : public QObject {Q_OBJECT public slots:void startProcessing(){processItem(0);}void processItem(int index){/* process items[index] ... */if (index < numberOfItems)QMetaObject::invokeMethod(this,"processItem",Qt::QueuedConnection,Q_ARG(int, index + 1));} };

因為這里沒有線程調(diào)用,所以它可以很容易的暫停/恢復(fù)/取消任務(wù),也可以很容易的得到計算結(jié)果。

?

一些例子

MD5 hash

?

參考

  • Bradley T. Hughes:?You’re doing it wrong…?[labs.qt.nokia.com], Qt Labs blogs, 2010-06-17
  • Bradley T. Hughes:?Threading without the headache?[labs.qt.nokia.com], Qt Labs blogs, 2006-12-04

轉(zhuǎn)自:http://www.cppblog.com/bitdewy/

轉(zhuǎn)載于:https://www.cnblogs.com/stevenpan/p/4031435.html

總結(jié)

以上是生活随笔為你收集整理的【转】【QT】 Threads, Events and QObjects的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。