Qt 學習之路:線程和 QObject

 

前面兩個章節咱們從事件循環和線程類庫兩個角度闡述有關線程的問題。本章咱們將深刻線程間得交互,探討線程和QObject之間的關係。在某種程度上,這纔是多線程編程真正須要注意的問題。git

 

如今咱們已經討論過事件循環。咱們說,每個 Qt 應用程序至少有一個事件循環,就是調用了QCoreApplication::exec()的那個事件循環。不過,QThread也能夠開啓事件循環。只不過這是一個受限於線程內部的事件循環。所以咱們將處於調用main()函數的那個線程,而且由QCoreApplication::exec()建立開啓的那個事件循環成爲主事件循環,或者直接叫主循環。注意,QCoreApplication::exec()只能在調用main()函數的線程調用。主循環所在的線程就是主線程,也被成爲 GUI 線程,由於全部有關 GUI 的操做都必須在這個線程進行。QThread的局部事件循環則能夠經過在QThread::run()中調用QThread::exec()開啓:github

記得咱們前面介紹過,Qt 4.4 版本之後,QThread::run()再也不是純虛函數,它會調用QThread::exec()函數。與QCoreApplication同樣,QThread也有QThread::quit()QThread::exit()函數來終止事件循環。編程

線程的事件循環用於爲線程中的全部QObjects對象分發事件;默認狀況下,這些對象包括線程中建立的全部對象,或者是在別處建立完成後被移動到該線程的對象(咱們會在後面詳細介紹「移動」這個問題)。咱們說,一個QObject的所依附的線程(thread affinity)是指它所在的那個線程。它一樣適用於在QThread的構造函數中構建的對象:安全

在咱們建立了MyThread對象以後,objotherObjyetAnotherObj的線程依附性是怎樣的?是否是就是MyThread所表示的那個線程?要回答這個問題,咱們必須看看到底是哪一個線程建立了它們:實際上,是調用了MyThread構造函數的線程建立了它們。所以,這些對象不在MyThread所表示的線程,而是在建立了MyThread的那個線程中。多線程

咱們能夠經過調用QObject::thread()能夠查詢一個QObject的線程依附性。注意,在QCoreApplication對象以前建立的QObject沒有所謂線程依附性,所以也就沒有對象爲其派發事件。也就是說,實際是QCoreApplication建立了表明主線程的QThread對象。併發

線程和QObject

咱們可使用線程安全的QCoreApplication::postEvent()函數向一個對象發送事件。它將把事件加入到對象所在的線程的事件隊列中,所以,若是這個線程沒有運行事件循環,這個事件也不會被派發。函數

值得注意的一點是,QObject及其全部子類都不是線程安全的(但都是可重入的)。所以,你不能有兩個線程同時訪問一個QObject對象,除非這個對象的內部數據都已經很好地序列化(例如爲每一個數據訪問加鎖)。記住,在你從另外的線程訪問一個對象時,它可能正在處理所在線程的事件循環派發的事件!基於一樣的緣由,你也不能在另外的線程直接delete一個QObject對象,相反,你須要調用QObject::deleteLater()函數,這個函數會給對象所在線程發送一個刪除的事件。post

此外,QWidget及其子類,以及全部其它 GUI 相關類(即使不是QObject的子類,例如QPixmap),甚至不是可重入的:它們只能在 GUI 線程訪問。ui

QObject的線程依附性是能夠改變的,方法是調用QObject::moveToThread()函數。該函數會改變一個對象及其全部子對象的線程依附性。因爲QObject不是線程安全的,因此咱們只能在該對象所在線程上調用這個函數。也就是說,咱們只能在對象所在線程將這個對象移動到另外的線程,不能在另外的線程改變對象的線程依附性。還有一點是,Qt 要求QObject的全部子對象都必須和其父對象在同一線程。這意味着:this

  • 不能對有父對象(parent 屬性)的對象使用QObject::moveToThread()函數
  • 不能在QThread中以這個QThread自己做爲父對象建立對象,例如:

    這是由於QThread對象所依附的線程是建立它的那個線程,而不是它所表明的線程。

Qt 還要求,在表明一個線程的QThread對象銷燬以前,全部在這個線程中的對象都必須先delete。要達到這一點並不困難:咱們只需在QThread::run()的棧上建立對象便可。

如今的問題是,既然線程建立的對象都只能在函數棧上,怎麼能讓這些對象與其它線程的對象通訊呢?Qt 提供了一個優雅清晰的解決方案:咱們在線程的事件隊列中加入一個事件,而後在事件處理函數中調用咱們所關心的函數。顯然這須要線程有一個事件循環。這種機制依賴於 moc 提供的反射:所以,只有信號、槽和使用Q_INVOKABLE宏標記的函數能夠在另外的線程中調用。

QMetaObject::invokeMethod()靜態函數會這樣調用:

主意,上面函數調用中出現的參數類型都必須提供一個公有構造函數,一個公有的析構函數和一個公有的複製構造函數,而且要使用qRegisterMetaType()函數向 Qt 類型系統註冊。

跨線程的信號槽也是相似的。當咱們將信號與槽鏈接起來時,QObject::connect()的最後一個參數將指定鏈接類型:

  • Qt::DirectConnection:直接鏈接意味着槽函數將在信號發出的線程直接調用
  • Qt::QueuedConnection:隊列鏈接意味着向接受者所在線程發送一個事件,該線程的事件循環將得到這個事件,而後以後的某個時刻調用槽函數
  • Qt::BlockingQueuedConnection:阻塞的隊列鏈接就像隊列鏈接,可是發送者線程將會阻塞,直到接受者所在線程的事件循環得到這個事件,槽函數被調用以後,函數纔會返回
  • Qt::AutoConnection:自動鏈接(默認)意味着若是接受者所在線程就是當前線程,則使用直接鏈接;不然將使用隊列鏈接

注意在上面每種狀況中,發送者所在線程都是可有可無的!在自動鏈接狀況下,Qt 須要查看信號發出的線程是否是與接受者所在線程一致,來決定鏈接類型。注意,Qt 檢查的是信號發出的線程,而不是信號發出的對象所在的線程!咱們能夠看看下面的代碼:

aSignal()信號在一個新的線程被髮出(也就是Thread所表明的線程)。注意,由於這個線程並非Object所在的線程(Object所在的線程和Thread所在的是同一個線程,回憶下,信號槽的鏈接方式與發送者所在線程無關),因此這裏將會使用隊列鏈接。

另一個常見的錯誤是:

這裏的obj發出aSignal()信號時,使用哪一種鏈接方式?答案是:直接鏈接。由於Thread對象所在線程發出了信號,也就是信號發出的線程與接受者是同一個。在aSlot()槽函數中,咱們能夠直接訪問Thread的某些成員變量,可是注意,在咱們訪問這些成員變量時,Thread::run()函數可能也在訪問!這意味着兩者併發進行:這是一個完美的致使崩潰的隱藏bug。

另一個例子可能更爲重要:

這個例子也會使用隊列鏈接。然而,這個例子比上面的例子更具隱蔽性:在這個例子中,你可能會以爲,Object所在Thread所表明的線程中被建立,又是訪問的Thread本身的成員數據。稍有不慎便會寫出這種代碼。

爲了解決這個問題,咱們能夠這麼作:Thread構造函數中增長一個函數調用:moveToThread(this)

實際上,這的確可行(由於Thread的線程依附性被改變了:它所在的線程成了本身),可是這並非一個好主意。這種代碼意味着咱們其實誤解了線程對象(QThread子類)的設計意圖:QThread對象不是線程自己,它們實際上是用於管理它所表明的線程的對象。所以,它們應該在另外的線程被使用(一般就是它本身所在的線程),而不是在本身所表明的線程中。

上面問題的最好的解決方案是,將處理任務的部分與管理線程的部分分離。簡單來講,咱們能夠利用一個QObject的子類,使用QObject::moveToThread()改變其線程依附性:

相關文章
相關標籤/搜索