1. 程式人生 > >【Qt開發】事件迴圈與執行緒 一

【Qt開發】事件迴圈與執行緒 一

初次讀到這篇文章,譯者感覺如沐春風,深刻體會到原文作者是花了很大功夫來寫這篇文章的,文章深入淺出,相信仔細讀完原文或下面譯文的讀者一定會有收穫。

由於原文很長,原文作者的行文思路是從事件迴圈逐漸延伸到執行緒使用的討論,譯者因時間受限,暫發表有關事件迴圈的譯文。另一半執行緒實用的譯文將近期公佈。文中有翻譯不當的地方,還請見諒。

介紹

執行緒是qt channel裡最流行的討論話題之一。許多人加入了討論並詢問如何解決他們在執行跨執行緒程式設計時所遇到的問題。

快速檢閱一下他們的程式碼,在發現的問題當中,十之八九遇到得最大問題是他們在某個地方使用了執行緒,而隨後又墜入了並行程式設計的陷阱。Qt中建立、執行執行緒的“易用”性、缺乏相關程式設計尤其是非同步網路程式設計知識或是養成的使用其它工具集的習慣、這些因素和Qt的訊號槽架構混合在一起,便經常使得人們自己把自己射倒在了腳下。此外,Qt對執行緒的支援是把雙刃劍:它即使得你在進行Qt多執行緒程式設計時感覺十分簡單,但同時你又必須對Qt所新新增許多的特性尤為小心,特別是與QObject的互動。

本文的目的不是教你如何使用執行緒、如何適當地加鎖,也不是教你如何進行並行開發或是如何寫可擴充套件的程式;關於這些話題,有很多好書,比如這個連結給的推薦讀物清單.  這篇文章主要是為了向讀者介紹Qt 4的事件迴圈以及執行緒使用,其目的在於幫助讀者們開發出擁有更好結構的、更加健壯的多執行緒程式碼,並回避Qt事件迴圈以及執行緒使用的常見錯誤。

先決條件

考慮到本文並不是一個執行緒程式設計的泛泛介紹,我們希望你有如下相關知識:

  • C++基礎;
  • Qt 基礎:QOjbects , 訊號/槽,事件處理;
  • 瞭解什麼是執行緒、執行緒與程序間的關係和作業系統;
  • 瞭解主流作業系統如何啟動、停止、等待並結束一個執行緒;
  • 瞭解如何使用mutexes, semaphores 和以及wait conditions 來建立一個執行緒安全/可重入的函式、資料結構、類。

本文我們將沿用如下的名詞解釋,即

  • 可重入 一個類被稱為是可重入的:只要在同一時刻至多隻有一個執行緒訪問同一個例項,那麼我們說多個執行緒可以安全地使用各自執行緒內自己的例項。 一個函式被稱為是可重入的:如果每一次函式的呼叫只訪問其獨有的資料(譯者注:全域性變數就不是獨有的,而是共享的),那麼我們說多個執行緒可以安全地呼叫這個函式。 也就是說,類和函式的使用者必須通過一些外部的加鎖機制來實現訪問物件例項或共享資料的序列化。
  • 執行緒安全  如果多個執行緒可以同時使用一個類的物件,那麼這個類被稱為是執行緒安全的;如果多個執行緒可以同時使用一個函式體裡的共享資料,那麼這個函式被稱為執行緒安全的。

(譯者注:   更多可重入(reentrant)和t執行緒安全(thread-safe)的解釋:  對於類,如果它的所有成員函式都可以被不同執行緒同時呼叫而不相互影響——即使這些呼叫是針對同一個類物件,那麼該類被定義為執行緒安全。 對於類,如果其不同例項可以在不同執行緒中被同時使用而不相互影響,那麼該類被定義為可重入。在Qt的定義中,在類這個層次,thread-safe是比reentrant更嚴格的要求)

事件與事件迴圈

Qt作為一個事件驅動的工具集,其事件和事件派發起到了核心的作用。本文將不會全面的討論這個話題,而是會聚焦於與執行緒相關的一些關鍵概念。想要了解更多的Qt事件系統專題參見 (這裡[doc.qt.nokia.com] 和 這裡[doc.qt.nokia.com] ) (譯者注:也歡迎參閱譯者寫的博文:淺議Qt的事件處理機制一

一個Qt的事件是代表了某件另人感興趣並已經發生的物件;事件與訊號的主要區別在於,事件是針對於與我們應用中一個具體目標物件(而這個物件決定了我們如何處理這個事件),而訊號發射則是“漫無目的”。從程式碼的角度來說,所有的事件例項是QEvent [doc.qt.nokia.com]的子類,並且所有的QObject的派生類可以過載虛擬函式QObject::event(),從而實現對目標物件例項事件的處理。

事件可以產生於應用程式的內部,也可以來源於外部;比如:

  • QKeyEvent和QMouseEvent物件代表了與鍵盤、滑鼠相關的互動事件,它們來自於視窗管理程式。
  • 當計時器開始計時,QTimerEvent 物件被髮送到QObject物件中,它們往往來自於作業系統。
  • 當一個子類物件被新增或刪除時,QChildEvent物件會被髮送到一個QObject物件重,而它們來自於你的應用程式內部

對於事件來講,一個重要的事情在於它們並沒有在事件產生時被立即派發,而是列入到一個事件佇列Event queue)中,等待以後的某一個時刻傳送。分配器(dispatcher )會遍歷事件佇列,並且將入棧的事件傳送到它們的目標物件當中,因此它們被稱為事件迴圈(Event loop). 從概念上講,下段程式碼描述了一個事件迴圈的輪廓:

  1. 1:  while (is_active)  
  2. 2:  {  
  3. 3:      while (!event_queue_is_empty)  
  4. 4:          dispatch_next_event();  
  5. 5:     
  6. 6:      wait_for_more_events();  
  7. 7:  }  

我們是通過執行QCoreApplication::exec()來進入Qt的主體事件迴圈的;這會引發阻塞,直至QCoreApplication::exit() 或者 QCoreApplication::quit() 被呼叫,進而結束迴圈。

這個“wait_for_more_events()” 函式產生阻塞,直至某個事件的產生。 如果我們仔細想想,會發現所有在那個時間點產生事件的實體必定是來自於外部的資源(因為當前所有內部事件派發已經結束,事件佇列裡也沒有懸而未決的事件等待處理),因此事件迴圈被這樣喚醒:

  • 視窗管理活動(鍵盤按鍵、滑鼠點選,與視窗的互動等等);
  • socket活動 (有可見的用來讀取的資料或者一個可寫的非阻塞Socket, 一個新的Socket連線的產生);
  • timers (即計時器開始計時)
  • 其它執行緒Post的事件(見後文)。

Unix系統中,視窗管理活動(即X11)通過Socket(Unix 域或者TCP/IP)通知應用程式(事件的產生),因為客戶端使用它們與X伺服器進行通訊。 如果我們決定用一個內部的socketpair(2)來實現跨執行緒的事件派發,那麼視窗管理活動需要喚醒的是

  • sockets;
  • timers;

這也是select(2) 系統呼叫所做的: 它為視窗管理活動監控了一組描述符,如果一段時間內沒有任何活動,它會超時。Qt所要做的是把系統呼叫select的返回值轉換為正確的QEvent子類物件,並將其列入事件佇列的棧中,現在你知道事件迴圈裡面裝著什麼東西了吧:)

為什麼需要執行事件迴圈?

下面的清單並不全,但你會有一幅全景圖,你應該能夠猜到哪些類需要使用事件迴圈。

  • Widgets 繪圖與互動: 當派發QPaintEvent事件時,QWidget::paintEvent() 將會被呼叫。QPaintEvent可以產生於內部的QWidget::update() ,也可以產生於外部的視窗管理(比如,一個顯示被隱藏的視窗)。同樣的,各種各樣的互動(鍵盤、滑鼠等)所對應的事件均需要事件迴圈來派發。
  • Timers: 長話短說,當select(2)或相類似的呼叫超時時,計時器開始計時,因此需要讓Qt通過返回事件迴圈讓那些呼叫為你工作。
  • Networking: 所以底層的Qt網路類(QTcpSocket, QUdpSocket, QTcpServer等)均被設計成非同步的。當你呼叫read()時,它們僅僅是返回已經可見的資料而已; 當你呼叫write()時,它們僅是將寫操作列入執行計劃表待稍後執行。 真正的讀寫僅發生於事件迴圈返回的時候。 請注意雖然Qt網路類提供了相應的同步方法(waitFor* 一族),但它們是不被推薦使用的,原因在於他們阻塞了正在等待的事件迴圈。向QNetworkAccessManager這樣的上層類,並不提供同步API 而且需要事件迴圈。

阻塞事件迴圈

在討論為什麼你永遠都不要阻塞事件迴圈之前,讓我們嘗試著再進一步弄明白到底“阻塞”意味著什麼。假定你有一個按鈕widget,它被按下時會emit一個訊號;還有一個我們下面定義的Worker物件連線了這個訊號,而且這個物件的槽做了很多耗時的事情。當你點選完這個按鈕後,從上之下的函式呼叫棧如下所示:

  1. main(intchar **)  
  2. QApplication::exec()  
  3. [...]  
  4. QWidget::event(QEvent *)  
  5. Button::mousePressEvent(QMouseEvent *)  
  6. Button::clicked()  
  7. [...]  
  8. Worker::doWork()  

在main()中,我們通過呼叫QApplication::exec() (如上段程式碼第2行所示)開啟了事件迴圈。視窗管理者傳送了滑鼠點選事件,該事件被Qt核心捕獲,並轉換成QMouseEvent ,隨後通過QApplication::notify() (notify並沒有在上述程式碼裡顯示)傳送到我們的widget的event()方法中(第4行)。因為Button並沒有過載event(),它的基類QWidget方法得以呼叫。 QWidget::event() 檢測出傳入的事件是一個滑鼠點選,並呼叫其專有的事件處理器,即Button::mousePressEvent() (第5行)。我們過載了 mousePressEvent方法,併發射了Button::clicked()訊號(第6行),該訊號激活了我們worker物件中十分耗時的Worker::doWork()槽(第8行)。(譯者注:如果你對這一段所描述得函式棧的更多細節,請參見淺議Qt的事件處理機制一

當worker物件在繁忙的工作時,事件迴圈在做什麼呢? 你也許猜到了答案:什麼也沒做!它分發了滑鼠點選事件,並且因等待event handler返回而被阻塞。我們阻塞了事件迴圈,也就是說,在我們的doWork()槽(第8行)幹完活之前再不會有事件被派發了,也再不會有pending的事件被處理。

當事件派發被就此卡住時,widgets 也將不會再重新整理自己(QPaintEvent物件將在事件佇列裡靜候),也不能有進一步地與widgets互動的事件發生,計時器也不會在開始計時,網路通訊也將變得遲鈍、停滯。更嚴重的是,許多視窗管理程式會檢測到你的應用不再處理事件,從而告訴使用者你的程式不再有響應(not responding). 這就是為什麼快速的響應事件並儘可能快的返回事件迴圈如此重要的原因

強制事件迴圈

那麼,對於需要長時間執行的任務,我們應該怎麼做才會不阻塞事件迴圈? 一個可行的答案是將這個任務移動另一個執行緒中:在一節,我們會看到如果去做。一個可能的方案是,在我們的受阻塞的任務中,通過呼叫QCoreApplication::processEvents() 人工地強迫事件迴圈執行。QCoreApplication::processEvents() 將處理所有事件佇列中的事件並返回給呼叫者。

另一個可選的強制地重入事件的方案是使用QEventLoop [doc.qt.nokia.com] 類,通過呼叫QEventLoop::exec() ,我們重入了事件迴圈,而且我們可以把訊號連線到QEventLoop::quit() 槽上使得事件迴圈退出,如下程式碼所示:

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

QNetworkReply 沒有提供一個阻塞式的API,而且它要求執行一個事件迴圈。我們進入到一個區域性QEventLoop,並且當迴應完成時,區域性的事件迴圈退出。

當重入事件迴圈是從“其他路徑”完成的則要非常小心:它可能會導致無盡的遞迴迴圈!讓我們回到Button這個例子。如果我們再在doWork() 槽裡面呼叫QCoreApplication::processEvents() ,這時使用者又一次點選了button,那麼doWork()槽將會再次被呼叫:

  1. main(intchar **)  
  2. QApplication::exec()  
  3. [...]  
  4. QWidget::event(QEvent *)  
  5. Button::mousePressEvent(QMouseEvent *)  
  6. Button::clicked()  
  7. [...]  
  8. Worker::doWork() // 實現,內部呼叫
  9. QCoreApplication::processEvents() // 我們人工的派發事件而且…
  10. [...]  
  11. QWidget::event(QEvent *) // 另一個滑鼠點選事件被髮送給Button
  12. Button::mousePressEvent(QMouseEvent *)  
  13. Button::clicked() // 這裡又一次emit了clicked() …
  14. [...]  
  15. Worker::doWork() // 完蛋! 我們已經遞迴地呼叫了doWork槽

一個快速並且簡單的臨時解決辦法是把QEventLoop::ExcludeUserInputEvents 傳遞給QCoreApplication::processEvents(), 也就是說,告訴事件迴圈不要派發任何使用者輸入事件(事件將簡單的呆在佇列中)。

同樣地,使用一個物件的deleteLater() 來實現非同步的刪除事件(或者,可能引發某種“關閉(shutdown)”的任何事件)則要警惕事件迴圈的影響。 (譯者注:deleteLater()將在事件迴圈中刪除物件並返回)

  1. 1:  QObject *object = new QObject;  
  2. 2:  object->deleteLater();  
  3. 3:  QEventLoop loop;  
  4. 4:  loop.exec();  
  5. 5:  /* 現在object是一個野指標! */

可以看到,我們並沒有用QCoreApplication::processEvents()  (從Qt 4.3之後,刪除事件不再被派發 ),但是我們確實用到了其他的區域性事件迴圈(像我們QEventLoop 啟動的這個迴圈,或者下面將要介紹的QDialog::exec())。

切記當我們呼叫QDialog::exec()或者 QMenu::exec()時,Qt進入了一個區域性事件迴圈。Qt 4.5 以後的版本,QDialog 提供了QDialog::open() 方法用來再不進入區域性迴圈的前提下顯示window-modal式的對話方塊

  1. 1:  QObject *object = new QObject;  
  2. 2:  object->deleteLater();  
  3. 3:  QDialog dialog;  
  4. 4:  dialog.exec();  
  5. 5:  /* 現在object是一個野指標! */

至此事件迴圈(event loop)的討論告一段落,接下來,我們要討論Qt的多執行緒:事件迴圈與執行緒二

 原文:http://blog.csdn.net/changsheng230/article/details/6101232

請尊重原創作品和譯文。轉載請保持文章完整性,並以超連結形式註明原始作者主站點地址,方便其他朋友提問和指正。

相關推薦

Qt開發事件迴圈執行

初次讀到這篇文章,譯者感覺如沐春風,深刻體會到原文作者是花了很大功夫來寫這篇文章的,文章深入淺出,相信仔細讀完原文或下面譯文的讀者一定會有收穫。 由於原文很長,原文作者的行文思路是從事件迴圈逐漸延伸到執行緒使用的討論,譯者因時間受限,暫發表有關事件迴圈的譯文。另一半執行緒實用的譯文將近期公佈。文中有翻譯不當

事件迴圈執行 (zz)

初次讀到這篇文章,譯者感覺如沐春風,深刻體會到原文作者是花了很大功夫來寫這篇文章的,文章深入淺出,相信仔細讀完原文或下面譯文的讀者一定會有收穫。由於原文很長,原文作者的行文思路是從事件迴圈逐漸延伸到執行緒使用的討論,譯者因時間受限,暫發表有關事件迴圈的譯文。另一半執行緒實用的

Android開發之FragmentAcitvity通信

對象 p s ets roi mit blog () open findview   上一篇我們講到與Fragment有關的經常使用函數,既然Fragment被稱為是“小Activity”。如今我們來講一下Fragment怎樣與Acitivity通信。

Qt開發QThread中的互斥、讀寫鎖、訊號量、條件變數

在gemfield的《從pthread到QThread》一文中我們瞭解了執行緒的基本使用,但是有一大部分的內容當時說要放到這片文章裡討論,那就是執行緒的同步問題。關於這個問題,gemfield在《從進 程到執行緒》中有一個比喻,有必要重新放在下面溫習下: ***************

Qt開發QT中顯示影象資料

一般影象資料都是以RGBRGBRGB……位元組流的方式(解碼完成後的原始影象流),我說成位元組流,那就表明R,G,B的值各佔一個位元組,在程式設計時表示的就是unsigned char * data。        我們先來看一下QT中的QImage物件。在載入data資料

Android開發wifi開關wifi連線(密碼連線)

過放蕩不羈的生活,容易得像順水推舟,但是要結識良朋益友,卻難如登天。—— 巴爾扎克 本文demo來自網路,找了好久找到的,後面自己做了些許修改,這裡對原始碼解析,愧於忘記哪裡出來了,感謝作者! 接下來就記錄一下wifi開發的一些學習心得,這邊先看幾張效果圖吧!     

Qt開發Qt5.9安裝

Qt5.9安裝包整合了全部資源,包括所有可選的不同版本及編譯器,不用再單獨下載,雖然大了點,但方便了很多。有時可能需要用VS搭配Qt來使用,但有時又想用QtCreator+mingw or QtCreator+vc的情況下。可以一次性安裝搞定,安裝時選擇需要的版本,安裝後

Qt開發Qt中顯示影象的兩種方法對比

在Qt中處理圖片一般都要用到QImage類,但是QImage的物件不能夠直接顯示出來,要想能看到圖片,初步發現有兩種方法。 一、QImage轉QPixmap,然後用QLabel::setPixmap() image=new QImage("D:/Temp/XX.jpg

Qt開發佈局控制元件之間的間距設定

void QLayout::setContentsMargins ( int left, int top, int right, int bottom ) Sets the left, top, right, and bottom margins to use ar

Qt開發QSplitter的使用和設定

 Qt庫版本:5.2.1     Qt Creator版本:3.0.1 1 QSplitter的用途     QSplitter使得使用者可以通過拖動子視窗之間的邊界來控制它們的大小,例如                                    

C/C++開發VS開發win32位x64位下各型別長度對比

64 位的優點:64 位的應用程式可以直接訪問 4EB 的記憶體和檔案大小最大達到4 EB(2 的 63 次冪);可以訪問大型資料庫。本文介紹的是64位下C語言開發程式注意事項。 1. 32 位和 64 位C資料型別 32和64位C語言內建資料型別,如下表所示:

Linux開發Qt開發QT 同時支援滑鼠和觸控式螢幕

現在 如果我要使用滑鼠 匯入環境變數 export QWS_MOUSE_PROTO=MouseMan:/dev/input/mice 使用觸控式螢幕,匯入環境變數 export QWS_MOUSE_PROTO=Tslib:/dev/input/touchscreen0 如果想同時兩個都支援   export

Qt開發QTextEdit 外觀屬性設定

一、給QTextEdit新增背景圖片,有下面兩種方法: QTextEdit* iEdit  = new QTextEdit(); 1:使用樣式表:      iEdit->setStyleSheet("background-image:url(:/bmp/D

Qt開發QTableWidget設定根據內容調整列寬和行高

QTableWidget要調整表格行寬主要涉及以下一個函式 1.resizeColumnsToContents();                      根據內容調整列寬            2.resizeColumnToContents(int col);  

Qt開發01-第一個Qt程序Hello World!

LL庫 label push 方式 dll 自動生成 一個 widget 圖片 一:說在前頭 我的第一份工作是做生產工具,當時用的MFC,IDE是VC6.0,現在想想真是古董級別,10年至今,微軟也一直沒有對MFC進行升級, 冥冥中感覺微軟自己都放棄MFC了,市場上貌似

Qt開發更改應用程序圖標和任務欄圖標

資源文件 同時 con 分享圖片 窗口圖標 程序 col 函數 ner 說明 實際開發過程中,生成的應用文件不會用默認的圖標,同時程序啟動後任務欄的圖標也需要修改,還有窗口的圖標,這樣顯得程序不那麽low。更改程序的圖標有多種方式,基於Qt Creator或vs開發的方式

學習筆記Java-Concurrent-多執行容器

BlockingQueue 阻塞佇列 高頻函式:   boolean put() 新增一個元素 沒有空間則一直阻塞等待   boolean add() 新增一個元素 沒有空間則丟擲IllegalStateException異常   boolean off

學習筆記Java-Concurrent-多執行測試模板

import java.util.concurrent.CountDownLatch; /** * 多執行緒測試模板 * * @author Mairuis * @date 2018/10/11 */ public class ConcurrentTest { public s

Java併發Java中的執行

Java中的執行緒池 執行流程 執行緒池的建立 提交任務 關閉執行緒池 參考 執行流程 處理流程如下: execute()方法執行示意圖如下: 執行緒池的建立 corePoolSize:執行緒池

(譯)Netty In Action第七章—事件迴圈執行模型

請尊重勞動成果,未經本人允許,拒絕轉載,謝謝! 這章包涵以下內容 - 執行緒模型概覽 - 事件迴圈概念和實現 - 任務排程 - 實現細節 簡單地說,執行緒模型指定了OS、程式語言、框架或應用程式的上下文中的執行緒管理的關鍵方面。執行緒創造的方式和時間明顯對於應用程