天天看點

事件循環與線程 一

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

由于原文很長,原文作者的行文思路是從事件循環逐漸延伸到線程使用的讨論,譯者因時間受限,暫發表有關事件循環的譯文。另一半線程實用的譯文将近期公布。文中有翻譯不當的地方,還請見諒。

介紹

線程是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:  while (is_active)
   2:  {
   3:      while (!event_queue_is_empty)
   4:          dispatch_next_event();
   5:   
   6:      wait_for_more_events();
   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對象連接配接了這個信号,而且這個對象的槽做了很多耗時的事情。當你點選完這個按鈕後,從上之下的函數調用棧如下所示:

main(int, char **)

QApplication::exec()

[...]

QWidget::event(QEvent *)

Button::mousePressEvent(QMouseEvent *)

Button::clicked()

[...]

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:  QNetworkAccessManager qnam;
   2:  QNetworkReply *reply = qnam.get(QNetworkRequest(QUrl(...)));
   3:  QEventLoop loop;
   4:  QObject::connect(reply, SIGNAL(finished()), &loop, SLOT(quit()));
   5:  loop.exec();
   6:  /* reply has finished, use it */       

QNetworkReply 沒有提供一個阻塞式的API,而且它要求運作一個事件循環。我們進入到一個局部QEventLoop,并且當回應完成時,局部的事件循環退出。

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

main(int, char **)

QApplication::exec()

[...]

QWidget::event(QEvent *)

Button::mousePressEvent(QMouseEvent *)

Button::clicked()

[...]

Worker::doWork() // 實作,内部調用

QCoreApplication::processEvents() // 我們人工的派發事件而且…

[...]

QWidget::event(QEvent *) // 另一個滑鼠點選事件被發送給Button

Button::mousePressEvent(QMouseEvent *)

Button::clicked() // 這裡又一次emit了clicked() …

[...]

Worker::doWork() // 完蛋! 我們已經遞歸地調用了doWork槽 

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

同樣地,使用一個對象的deleteLater() 來實作異步的删除事件(或者,可能引發某種“關閉(shutdown)”的任何事件)則要警惕事件循環的影響。 (譯者注:deleteLater()将在事件循環中删除對象并傳回)

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

可以看到,我們并沒有用QCoreApplication::processEvents()  (從Qt 4.3之後,删除事件不再被派發 ),但是我們确實用到了其他的局部事件循環(像我們QEventLoop 啟動的這個循環,或者下面将要介紹的QDialog::exec())。

切記當我們調用QDialog::exec()或者 QMenu::exec()時,Qt進入了一個局部事件循環。Qt 4.5 以後的版本,QDialog 提供了QDialog::open() 方法用來再不進入局部循環的前提下顯示window-modal式的對話框

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

至此事件循環(event loop)的讨論告一段落,接下來,我們要讨論Qt的多線程:事件循環與線程二

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

繼續閱讀