QT基於UDP通訊的多執行緒程式設計問題
近來用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訊號。定義如下:
// define Object class
- class SomeObject : public QObject
- {
- Q_OBJECT
- public:
- SomeObject(QObject* parent=0) : QObject(parent) {}
- void callEmitSignal() // 用於傳送訊號的函式
- {
- emit someSignal();
- }
- signals:
- void someSignal();
- };
然後再定義一個從QThread繼承的執行緒類SubThread,它包含一個SomeObject的物件指標obj,另外有一個slot函式someSolt,定義如下:
- class SubThread : public QThread
- {
- Q_OBJECT
- public:
- SubThread(QObject* parent=0) : QThread(parent){}
- virtual ~SubThread()
- {
- if (obj!=NULL) delete obj;
- }
- public slots:
- // slot function connected to obj's someSignal
- void someSlot();
- public:
- SomeObject * obj;
- };
- // slot function connected to obj's someSignal
- void SubThread::someSlot()
- {
- QString msg;
- msg.append(this->metaObject()->className());
- msg.append("::obj's thread is ");
- if (obj->thread() == qApp->thread())
- {
- msg.append("MAIN thread;");
- }
- elseif (obj->thread() == this)
- {
- msg.append("SUB thread;");
- }
- else
- {
- msg.append("OTHER thread;");
- }
- msg.append(" someSlot executed in ");
- if (QThread::currentThread() == qApp->thread())
- {
- msg.append("MAIN thread;");
- }
- elseif (QThread::currentThread() == this)
- {
- msg.append("SUB thread;");
- }
- else
- {
- msg.append("OTHER thread;");
- }
- qDebug() << msg;
- quit();
- }
這裡someSlot函式主要輸出了obj所在的執行緒和slot函式執行執行緒。
接著從SubThread又繼承了3個執行緒類,分別是SubThread1, SubThread2, SubThread3.分別實現執行緒的run函式。定義如下:
- // define sub thread class 1
- class SubThread1 : public SubThread
- {
- Q_OBJECT
- public:
- SubThread1(QObject* parent=0);
- // reimplement run
- void run();
- };
- class SubThread2 : public SubThread
- {
- Q_OBJECT
- public:
- SubThread2(QObject* parent=0);
- // reimplement run
- void run();
- };
- class SubThread3 : public SubThread
- {
- Q_OBJECT
- public:
- SubThread3(QObject* parent=0);
- // reimplement run
- void run();
- };
在主程式中分別建立3個不同的執行緒並執行,檢視執行結果。
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- SubThread1* t1 = new SubThread1(&a); //由主執行緒建立
- t1->start();
- SubThread2* t2 = new SubThread2(&a); //由主執行緒建立
- t2->start();
- SubThread3* t3 = new SubThread3(&a); //由主執行緒建立
- t3->start();
- return a.exec();
- }
下面我們來分析不同寫法的程式,其obj物件所在的執行緒空間和someSlot函式執行的執行緒空間分別是怎樣的。
首先看SubThread1的實現:
- ////////////////////////////////////////////////////////
- // class SubThread1
- ////////////////////////////////////////////////////////
- SubThread1::SubThread1(QObject* parent)
- : SubThread(parent)
- {
- obj = new SomeObject();//由主執行緒建立
- connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));
- }
- // reimplement run
- void SubThread1::run()
- {
- obj->callEmitSignal();
- exec();
- }
可以看到,obj是在建構函式中被建立的,那麼建立obj物件的執行緒也就是建立SubThread1的執行緒,一般是主執行緒,而不是SubThread1所代表的執行緒。同時由於obj和this(即t1)都位於主執行緒,所以someSlot函式也是由主執行緒來執行的。
而線上程SubThread2中,我們把obj物件的建立放到子執行緒的run函式中,那麼obj物件的執行緒就應該SubThread2代表的執行緒,即t2,就不再是主執行緒了。
- ////////////////////////////////////////////////////////
- // class SubThread2
- ////////////////////////////////////////////////////////
- SubThread2::SubThread2(QObject* parent)
- : SubThread(parent)
- {
- obj=0;
- }
- // reimplement run
- void SubThread2::run()
- {
- obj = new SomeObject(); //由當前子執行緒建立
- connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()));
- obj->callEmitSignal();
- exec();
- }
同時,在connect函式中由於obj和this(這裡是t2)不是在同一個執行緒中,因此會採用QueuedConnection的方式,其slot函式由this物件所在的執行緒即主執行緒來執行。這裡有一個特別容易誤解的地方,就是這個slot函式雖然是子執行緒SubThread2的一個成員函式,connect操作也是在子執行緒內完成的,但是該函式的執行卻不在子執行緒內,而是在主執行緒內。
那麼如果想讓相應的slot函式在子執行緒內執行,該如何做呢?在子執行緒的run函式中建立obj物件的同時,在執行connect時指定連線方式為DirectConnection,這樣就可以使slot函式在子執行緒中執行,因為DirectConnection的方式始終由sender物件的執行緒執行。如
- ////////////////////////////////////////////////////////
- // class SubThread3
- ////////////////////////////////////////////////////////
- SubThread3::SubThread3(QObject* parent)
- : SubThread(parent)
- {
- obj=0;
- }
- // reimplement run
- void SubThread3::run()
- {
- obj = new SomeObject();
- connect(obj, SIGNAL(someSignal()), this, SLOT(someSlot()),
- Qt::DirectConnection);
- obj->callEmitSignal();
- exec();
- }
最後,該程式的執行結果應該是:
- "SubThread1::obj's thread is MAIN thread; someSlot executed in MAIN thread;"
- "SubThread2::obj's thread is SUB thread; someSlot executed in MAIN thread;"
- "SubThread3::obj's thread is SUB thread; someSlot executed in SUB thread;"