事件循環與線程 一(zz)

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

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

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

 

介紹

線程是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的多線程:事件循環與線程二

 

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


發佈了0 篇原創文章 · 獲贊 1 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章