Qt 多線程編程中的對象線程與函數執行線程

近來用Qt編寫一段多線程的TcpSocket通信程序,被其中Qt中報的幾個warning搞暈了,一會兒是說“Cannot create children for a parent that is in a different thread”,有時候又是“QSocketNotifier: socket notifiers cannot be enabled from another thread”,還經常又Assert failure:Cannot send events toobjects owned by a different thread,從而導致程序崩潰。

    爲徹底搞清原因並解決問題,在查閱大量資料和Qt文檔之後,理清了其中的機制,也對多線程編程中的QObject對象創建以及connect執行有更清楚的認識:

    1. 一個對象的線程就是創建該對象時的線程,而不論該對象的定義是保存在那個線程中;

    2. QObject的connect函數有幾種連接方式,

      a) DirectConnection,信號發送後槽函數立即執行,由sender的所在線程執行;

      b) QueuedConnection,信號發送後返回,相關槽函數由receiver所在的線程在返回到事件循環後執行;

      c) 默認使用的是Qt::AutoConnection,當sender和receiver在同一個線程內時,採用DirectConnection的方式,當sender和receiver在不同的線程時,採用QueuedConnection的方式。

    爲了更清楚的理解這些問題,在此特編了個小例子說明一下。首先定義一個從QObject繼承的類SomeObject,包含一個信號someSignal和一個成員函數callEmitSignal,此函數用於發送前面的someSignal信號。定義如下:

class SomeObject : public QObject  
  1. {  
  2.     Q_OBJECT  
  3. public:  
  4.     SomeObject(QObject* parent=0) : QObject(parent) {}  
  5.     void callEmitSignal()  // 用於發送信號的函數  
  6.     {  
  7.         emit someSignal();  
  8.     }  
  9. signals:  
  10.     void someSignal();  
  11. };  
  12.  

然後再定義一個從QThread繼承的線程類SubThread,它包含一個SomeObject的對象指針obj,另外有一個slot函數someSolt,定義如下:

  1. class SubThread : public QThread  
  2. {  
  3.     Q_OBJECT  
  4. public:  
  5.     SubThread(QObject* parent=0) : QThread(parent){}  
  6.     virtual ~SubThread()  
  7.     {  
  8.         if (obj!=NULL) delete obj;  
  9.     }  
  10. public slots:  
  11.     // slot function connected to obj's someSignal  
  12.     void someSlot();  
  13. public:  
  14.     SomeObject * obj;  
  15. };  
  16. // slot function connected to obj's someSignal  
  17. void SubThread::someSlot()  
  18. {  
  19.     QString msg;  
  20.     msg.append(this->metaObject()->className());  
  21.     msg.append("::obj's thread is ");  
  22.     if (obj->thread() == qApp->thread())  
  23.     {  
  24.         msg.append("MAIN thread;");  
  25.     }  
  26.     else if (obj->thread() == this)  
  27.     {  
  28.         msg.append("SUB thread;");  
  29.     }  
  30.     else  
  31.     {  
  32.         msg.append("OTHER thread;");  
  33.     }  
  34.     msg.append(" someSlot executed in ");  
  35.     if (QThread::currentThread() == qApp->thread())  
  36.     {  
  37.         msg.append("MAIN thread;");  
  38.     }  
  39.     else if (QThread::currentThread() == this)  
  40.     {  
  41.         msg.append("SUB thread;");  
  42.     }  
  43.     else  
  44.     {  
  45.         msg.append("OTHER thread;");  
  46.     }  
  47.     qDebug() << msg;  
  48.     quit();  
  49. }

這裏someSlot函數主要輸出了obj所在的線程和slot函數執行線程。

    接着從SubThread又繼承了3個線程類,分別是SubThread1, SubThread2, SubThread3.分別實現線程的run函數。定義如下:

  1. class SubThread1 : public SubThread  
  2. {  
  3.     Q_OBJECT  
  4. public:  
  5.     SubThread1(QObject* parent=0);  
  6.     // reimplement run  
  7.     void run();  
  8. };  
  9. class SubThread2 : public SubThread  
  10. {  
  11.     Q_OBJECT  
  12. public:  
  13.     SubThread2(QObject* parent=0);  
  14.     // reimplement run  
  15.     void run();  
  16. };  
  17. class SubThread3 : public SubThread  
  18. {  
  19.     Q_OBJECT  
  20. public:  
  21.     SubThread3(QObject* parent=0);  
  22.     // reimplement run  
  23.     void run();  
  24. };

    在主程序中分別創建3個不同的線程並運行,查看運行結果。

  1. int main(int argc, char *argv[])  
  2. {  
  3.     QCoreApplication a(argc, argv);  
  4.     SubThread1* t1 = new SubThread1(&a); //由主線程創建  
  5.     t1->start();  
  6.     SubThread2* t2 = new SubThread2(&a); //由主線程創建  
  7.     t2->start();  
  8.     SubThread3* t3 = new SubThread3(&a); //由主線程創建  
  9.     t3->start();  
  10.     return a.exec();  
  11. }</span>  

    下面我們來分析不同寫法的程序,其obj對象所在的線程空間和someSlot函數執行的線程空間分別是怎樣的。

    首先看SubThread1的實現:

  1. SubThread1::SubThread1(QObject* parent)  
  2.     : SubThread(parent)  
  3. {  
  4.     obj = new SomeObject();//由主線程創建  
  5.     connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));  
  6. }  
  7. // reimplement run  
  8. void SubThread1::run()  
  9. {  
  10.     obj->callEmitSignal();  
  11.     exec();  
  12. }</span>  

可以看到,obj是在構造函數中被創建的,那麼創建obj對象的線程也就是創建SubThread1的線程,一般是主線程,而不是SubThread1所代表的線程。同時由於obj和this(即t1)都位於主線程,所以someSlot函數也是由主線程來執行的。

    而在線程SubThread2中,我們把obj對象的創建放到子線程的run函數中,那麼obj對象的線程就應該SubThread2代表的線程,即t2,就不再是主線程了。
  1. SubThread2::SubThread2(QObject* parent)  
  2.     : SubThread(parent)  
  3. {  
  4.     obj=0;  
  5. }  
  6. // reimplement run  
  7. void SubThread2::run()  
  8. {  
  9.     obj = new SomeObject(); //由當前子線程創建  
  10.     connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));  
  11.     obj->callEmitSignal();  
  12.     exec();  
  13. }</span>  

同時,在connect函數中由於obj和this(這裏是t2)不是在同一個線程中,因此會採用QueuedConnection的方式,其slot函數由this對象所在的線程即主線程來執行。這裏有一個特別容易誤解的地方,就是這個slot函數雖然是子線程SubThread2的一個成員函數,connect操作也是在子線程內完成的,但是該函數的執行卻不在子線程內,而是在主線程內。

    那麼如果想讓相應的slot函數在子線程內執行,該如何做呢?在子線程的run函數中創建obj對象的同時,在執行connect時指定連接方式爲DirectConnection,這樣就可以使slot函數在子線程中運行,因爲DirectConnection的方式始終由sender對象的線程執行。如
  1. SubThread3::SubThread3(QObject* parent)  
  2.     : SubThread(parent)  
  3. {  
  4.     obj=0;  
  5. }  
  6. // reimplement run  
  7. void SubThread3::run()  
  8. {  
  9.     obj = new SomeObject();  
  10.     connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()),  
  11.             Qt::DirectConnection);  
  12.     obj->callEmitSignal();  
  13.     exec();  
  14. }

    最後,該程序的運行結果應該是:

  1. "SubThread1::obj's thread is MAIN thread; someSlot executed in MAIN thread;"   
  2. "SubThread2::obj's thread is SUB thread; someSlot executed in MAIN thread;"   
  3. "SubThread3::obj's thread is SUB thread; someSlot executed in SUB thread;" 


在這裏順便解釋下Qt::ConnectType這個枚舉:

signal/slot在底層會使用三種方式傳遞消息。參見QObject::connect()方法:
bool QObject::connect ( const QObject * sender, const char * signal, const QObject * receiver, const char * method, Qt::ConnectionType type = Qt::AutoCompatConnection )
最後一個參數是就是傳遞消息的方式了,有四個取值:

Qt::DirectConnection
When emitted, the signal is immediately delivered to the slot.
假設當前有4個slot連接到QPushButton::clicked(bool),當按鈕被按下時,QT就把這4個slot按連接的時間順序調用一遍。顯然這種方式不能跨線程(傳遞消息)。

Qt::QueuedConnection
When emitted, the signal is queued until the event loop is able to deliver it to the slot.
假設當前有4個slot連接到QPushButton::clicked(bool),當按鈕被按下時,QT就把這個signal包裝成一個 QEvent,放到消息隊列裏。QApplication::exec()或者線程的QThread::exec()會從消息隊列裏取消息,然後調用 signal關聯的幾個slot。這種方式既可以在線程內傳遞消息,也可以跨線程傳遞消息。

Qt::BlockingQueuedConnection
Same as QueuedConnection, except that the current thread blocks until the slot has been delivered. This connection type should only be used for receivers in a different thread. Note that misuse of this type can lead to dead locks in your application.
與Qt::QueuedConnection類似,但是會阻塞等到關聯的slot都被執行。這裏出現了阻塞這個詞,說明它是專門用來多線程間傳遞消息的。

Qt::AutoConnection
If the signal is emitted from the thread in which the receiving object lives, the slot is invoked directly, as with Qt::DirectConnection; otherwise the signal is queued, as with Qt::QueuedConnection.
這種連接類型根據signal和slot是否在同一個線程裏自動選擇Qt::DirectConnection或Qt::QueuedConnection

這樣看來,第一種類型的效率肯定比第二種高,畢竟第二種方式需要將消息存儲到隊列,而且可能會涉及到大對象的複製(考慮sig_produced(BigObject bo),bo需要複製到隊列裏)。

Qt::UniqueConnection

This is a flag that can be combined with any one of the above connection types, using a bitwise OR. When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists (i.e. if the same signal is already connected to the same slot for the same pair of objects). 

這個枚舉我沒有用過,從翻譯上理解,和上面的枚舉或結合使用,而且相同的信號中只有一個信號與槽進行連接!

我還是不太理解,知道的可以留言!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章