1. 程式人生 > >事件迴圈與執行緒 一(zz)

事件迴圈與執行緒 一(zz)

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

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

介紹

執行緒是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的多執行緒:事件迴圈與執行緒二

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


相關推薦

事件迴圈執行 (zz)

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

【Qt開發】事件迴圈執行

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

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

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

java事件驅動執行結合

import java.lang.*; import java.awt.*; import java.awt.event.*; import java.awt.event.ActionListener

C++網路程式設計實戰專案--Sinetlib網路庫(3)——事件迴圈執行呼叫

上一篇文章講了Reactor模式的關鍵結構I/O複用和事件分發,現在我們來關注一下它們的使用。 事件迴圈 我們已經實現了一個Epoller類來實現I/O複用,具體的使用方法就是Epoller::Poll()函式等待事件的發生,該函式有一個超時時間,超過這個時間即

、JVM(HotSpot)Java記憶體模型執行

注:本博文主要是基於JDK1.7會適當加入1.8內容。 1、Java記憶體模型 記憶體模型:在特定的操作協議下,對特定的記憶體或快取記憶體進行讀寫訪問的抽象過程。不同的物理機擁有不一樣的記憶體模型,而Java虛擬機器也擁有自己的記憶體模型。 主要目標:定義程式中各個變數的訪問規則,

程序執行)——程序

序言: 我們知道多執行緒是現代作業系統中一個很重要的組成部分。它經常在面試過程中或者在實際運用過程中都會碰到的一個比較難的一個問題,所以這篇文章所屬的一個系列,將淺顯的記錄下博主的多執行緒學習的一個過程。 首先呢,說起執行緒的起源,我們不得不提就是程序的概念。然而一開始也並不是直接就有

深入理解JVM(十)——Java記憶體模型執行

計算機運算的速度,與它的儲存和通訊子系統相差太大,大量的時間花費在磁碟IO,網路通訊和資料庫上。 衡量一個服務效能的高低好壞,每秒事務處理數TPS是最重要的指標。 對於計算量相同的任務,程式執行緒併發協調的越有條不紊,效率越高;反之,執行緒之間頻繁阻塞或是死鎖,將大大降低併發能力。

Java執行執行安全,開啟多執行及每執行迴圈10次對類進行輸出測試

最近看到執行緒問題,emmm~腦闊回想到計算機作業系統貌似又講,不過上課睡覺覺去啦哈哈哈,java課老師莫得講~ 然歸正傳,今對執行緒進行查閱及測試,做一下筆記,有錯之處還請指出,謝謝~上程式碼之前呢先說一哈前傳 執行緒是程序中的最小執行單位:    手機呢會有很多單獨

jdk原始碼解析(十)——Java記憶體模型執行

前面我們瞭解了Java的編譯和執行,這裡在講解一下高效併發(Java記憶體模型與執行緒)在瞭解記憶體模型與執行緒之前,我們先要了解一些東西。 1 硬體效率與一致性  計算併發執行的執行和充分利用計算機處理器的效能兩者看來是互為因果的,而在大多數的時候,計算機的處理速度不止是在處理器

下單例模式,考慮執行安全執行不安全的情況

凡是提到設計模式,面試官很喜歡問最簡單的單例模式。 方法一 單例模式最簡單的寫法如下 public class SingletonPatternA { private static SingletonPatternA instance =

執行基礎知識(執行的概念及建立任務執行

執行緒的概念及建立任務與執行緒 引言 多執行緒使得程式中的多個任務可以同時執行。 java的重要功能之一就是內部支援多執行緒————在一個程式中運行同時執行多個任務。在許多程式設計語言中,多執行緒都是通過呼叫依賴於系統的過程或函式來實現的。在本文中,將介紹執行緒的該

Netty 入門():基本元件執行模型

  Netty 的學習內容主要是圍繞 TCP 和 Java NIO 這兩個點展開的,後文中所有的內容如果沒有特殊說明,那麼所指的內容都是與這兩點相關的。由於 Netty 是基於 Java NIO 的 API 之上構建的網路通訊框架,Java NIO 中的幾個元件,都能在 Netty 中找到對應的封裝。下面我們

Android執行執行池(

前言,學習安卓很久了,一直也沒有學部落格的習慣,下決心從今天開始要養成寫部落格總結學習經驗的好習慣! 一.Android中執行緒與執行緒池的簡介 在Android中執行緒主要可以分為兩大類:一個用於處理介面相關與使用者互動的執行緒-主執行緒;一個用於處理耗時任務-子執行緒

C#——基於委託事件的多執行通訊(同樣適用於非UI執行間通訊)

在研究c# 執行緒之間通訊時,發現傳統的方法大概有三種 ①全域性變數,由於同一程序下的多個程序之間共享資料空間,所以使用全域性變數是最簡單的方法,但要記住使用volatile進行限制。 ②執行緒之間傳送訊息(這個隨後文章中會討論到)。 ③CEvent為MFC中的一

夜搞懂 | Java 記憶體模型執行

前言 本文已經收錄到我的 Github 個人部落格,歡迎大佬們光臨寒舍: 我的 GIthub 部落格 學習導圖 一.為什麼要學習記憶體模型與執行緒? 併發處理的廣泛應用是 Amdah1 定律代替摩爾定律成為計算機效能發展源動力的根本原因,也是人類壓制計算機運算能力的最有力武器 執行緒通訊是指執行緒

C#多執行程式設計()程序執行

一、 程序         簡單來說,程序是對資源的抽象,是資源的容器,在傳統作業系統中,程序是資源分配的基本單位,而且是執行的基本單位,程序支援併發執行,因為每個程序有獨立的資料,獨立的堆疊空間。一個程式想要併發執行,開多個程序即可。

12 認識程序執行 (進階)

認識程序與執行緒(python)   一段時間沒有更新部落格了,今天和大家講講關於 python 程序和執行緒的知識點。(個人心得,多多指教!) 階段一:併發與並行的深入理解 ​ 並行一定是併發,但併發不一定是並行。 ​ 並行是相對的,並行是絕對的。 1、關於並行與併發的問題引入: 問題一: 計算

執行()

執行緒 Thread 1)應用程式以程序為單位執行,一個程序之內可以分為一到多個執行緒 2)作業系統有個元件叫任務排程器,將cpu的時間分給不同程式使用,微觀序列(單核),巨集觀並行(多核) 3)可以使用jconsle 來檢視某個java程序中執行緒的執行情況,包括死鎖等  

【Linux系統學習】程序執行

程序執行新程式 程序等待 wait()函式 程序執行新程式 fork()/exec()組合是典型的Linux