QThread繼承自QObject,它發射信號(signals)以表明線程執行開始或結束,並提供了一些槽函數(slots)。
更有趣的是,QObjects可以在多線程中使用,發射信號以在其它線程中調用槽函數,並且向“存活”於其它線程中的對象發送事件(post events)。這是可能的,因為每一個線程都擁有它自身的事件循環(event loop)。
QObject是可重入的。它的大多數非GUI子類,例如:QTimer、QTcpSocket、QUdpSocket和QProcess,也都是可重入的,這使得在多線程中同時使用這些類成為可能。注意:這些類被設計成在單一線程中創建和使用的,在一個線程中創建一個對象而在另一個線程中調用該對象的函數,不保證能行得通。需要注意有三個約束:
一個QObject類型的孩子必須總是被創建在它的父親所被創建的線程中。這意味著,除了別的以外,永遠不要把QThread對象(this)作為該線程中創建的一個對象的父親(因為QThread對象自身被創建在另外一個線程中)。
事件驅動的對象可能只能被用在一個單線程中。特別是,這適用於計時器機制(timer mechanism)和網絡模塊。例如:你不能在不屬於這個對象的線程中啟動一個定時器或連接一個socket,必須保證在刪除QThread之前刪除所有創建在這個線程中的對象。在run()函數的實現中,通過在棧中創建這些對象,可以輕松地做到這一點。
雖然QObject是可重入的,但GUI類,尤其是QWidget及其所有子類都不是可重入的,它們只能被用在主線程中。如前面所述,QCoreApplication::exec()必須也從那個線程被調用。
在實踐中,只能在主線程而非其它線程中使用GUI的類這一點,可以很輕易地被解決:將耗時操作放在一個單獨的worker線程中,當worker線程結束後在主線程中由屏幕顯示結果。這個方法被用來實現Mandelbrot Example和the Blocking Fortune Client Example。
一般來說,在QApplication之前就創建QObject是不行的,會導致奇怪的崩潰或退出,取決於平台。這意味著,也不支持QObject的靜態實例。一個單線程或多線程的應用程序應該先創建QApplication,並最後銷毀QObject。
每個線程都有它自己的事件循環。初始線程通過QCoreApplication::exec()來啟動它的事件循環, 或者對於單對話框的GUI應用程序,有些時候用QDialog::exec(),其它線程可以用QThread::exec()來啟動事件循環。就像QCoreApplication,QThread提供一個exit(int)函數和quit()槽函數.
一個線程中的事件循環使得該線程可以利用一些非GUI的、要求有事件循環存在的Qt類(例如:QTimer、QTcpSocket、和QProcess)。它也使得連接一些線程的信號到一個特定線程的槽函數成為可能。這一點將會在下面的“跨線程的信號和槽”有詳細介紹。
一個QObject實例被稱為存活於它所被創建的線程中。關於這個對象的事件被分發到該線程的事件循環中。可以用QObject::thread()方法獲取一個QObject所處的線程。
QObject::moveToThread()函數改變一個對象和它的孩子的線程所屬性。(如果該對象有父親的話,它不能被移動到其它線程中)。
從另一個線程(不是該QObject對象所屬的線程)對該QObject對象調用delete方法是不安全的,除非你能保證該對象在那個時刻不處理事件,使用QObejct::deleteLater()更好。一個DeferredDelete類型的事件將被提交(posted),而該對象的線程的事件循環最終會處理這個事件。默認情況下,擁有一個QObject的線程就是創建QObject的那個線程,而不是QObject::moveToThread()被調用後的。
如果沒有事件循環運行,事件將不會傳遞給對象。例如:你在一個線程中創建了一個QTimer對象,但從沒有調用exec(),那麼,QTimer就永遠不會發射timeout()信號,即使調用deleteLater()也不行。(這些限制也同樣適用於主線程)。
利用線程安全的方法QCoreApplication::postEvent(),你可以在任何時刻給任何線程中的任何對象發送事件,這些事件將自動被分發到該對象所被創建的線程事件循環中。
所有的線程都支持事件過濾器,而限制是監控對象必須和被監控對象存在於相同的線程中。類似的,QCoreApplication::sendEvent()(不同於postEvent())只能將事件分發到和該函數調用者相同的線程中的對象。
QObject及其所有子類都不是線程安全的。這包含了整個事件交付系統。重要的是,切記事件循環可能正在向你的QObject子類發送事件,當你從另一個線程訪問該對象時。
如果你正在調用一個QObject子類的函數,而該子類對象並不存活於當前線程中,並且該對象是可以接收事件的,那麼你必須用一個mutex保護對該QObject子類的內部數據的所有訪問,否則,就有可能發生崩潰和非預期的行為。
同其它對象一樣,QThread對象存活於該對象被創建的線程中 – 而並非是在QThread::run()被調用時所在的線程。一般來說,在QThread子類中提供槽函數是不安全的,除非用一個mutex保護成員變量。
另一方面,可以在QThread::run()的實現中安全地發射信號,因為信號發射是線程安全的。
Qt支持如下的信號-槽連接類型:
Auto Connection(默認):如果信號在接收者所依附的線程內發射,則等同於Direct Connection。否則,等同於Queued Connection。
Direct Connection:當信號發射後,槽函數立即被調用。槽函數在信號發射者所在的線程中執行,而未必需要在接收者的線程中。
Queued Connection:當控制權回到接受者所在線程的事件循環時,槽函數被調用。槽函數在接收者的線程中執行。
Blocking Queued Connection:槽函數的調用情形和Queued Connection相同,不同的是當前的線程會阻塞住,直到槽函數返回。
注意:在同一個線程中使用這種類型進行連接會導致死鎖。
Unique Connection:行為與Auto Connection相同,但是連接只會在“不會與已存在的連接相同”時建立,也就是:如果相同的信號已經被連接到相同的槽函數,那麼連接就不會被再次建立,並且connect()會返回false。
通過傳遞一個參數給connect()來指定連接類型。要知道,如果一個事件循環運行在接收者的線程中,而發送者和接收者位於不同的線程時,使用Direct Connection是不安全的。同樣的原因,調用存活於另一個線程中的對象的任何函數都是不安全的。
QObject::connect()本身是線程安全的。
Mandelbrot Example使用了Queued Connection來連接一個worker線程和主線程。為了避免凍結主線程的事件循環(即避免因此而凍結了應用的UI),所有的曼德爾布羅特分形計算(Mandelbrot fractal computation)都是在一個單獨的worker線程中完成的,線程結束一個計算時發射一個信號。
同樣,Blocking Fortune Client Example使用了一個單獨的線程來和TCP server進行異步通信。