1 說下背景
1.1 常規方式存在的問題
一般來說,在Qt中使用線程,最常規的做法是繼承QThread,重寫run函數,調用start函數,run函數里邊的代碼就會在新的線程中執行了。這樣做有點麻煩,要繼承、重寫,還容易出錯,最典型的錯誤如下:
QObject: Cannot create children for a parent that is in a different thread.
這個錯誤想必所有Qter都犯過,如果你沒發過這個錯誤,請接受我五體投地一拜。這個錯誤的原因也很簡單,run函數是在新的線程中執行,在run函數中實例化對象時入了this參數,但是QThread對象(也就是this)本身是附屬於主線程的,他兩屬於不同的時空的對象,簡單來說你在新的線程中創建了一個對象,同時為這個對象指定了一個另一個線程的對象為父對象,這樣是不對的,所以會報上面的警告。
也好解決,一般來說打開線程的事件循環(執行exec()),然后在run函數中創建局部變量(對象)即可。
1.2 推薦的方式
QObject提供了moveToThread接口,可以將QObject對象移動到新的線程,此時有個注意點,就是此時與該對象的交互只能通過信號槽的方式了,如果在主線程直接調用該對象函數,那么該函數是不會在新的線程中執行的。雖然moveToThread該接口十分簡潔,也推薦使用,但是要想用得好也不是那么容易,下面以一個簡單例子來說明。
2 舉一個例子
2.1 前提
這里定義一個類MyObject,該類包含一個成員socket,本例子目標是過moveToThread將該類以及成員移動到新的線程。
2.2 成員變量的方式
這里直接定義一個QThread成員變量,用於將MyObject移動到新的線程,代碼如下:
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <QObject>
#include <QThread>
#include <QUdpSocket>
class MyObject : public QObject
{
Q_OBJECT
public:
explicit MyObject(QObject *parent = 0);
~MyObject();
private:
QThread thread;
QUdpSocket socket;
};
#endif // MYOBJECT_H
#include "MyObject.h"
#include <QDebug>
MyObject::MyObject(QObject *parent) : QObject(parent)
{
qDebug() << "main thread" << QThread::currentThread();
this->moveToThread(&thread);
thread.start();
qDebug() << "socket thread" << socket.thread();
qDebug() << "MyObject thread" << this->QObject::thread();
}
MyObject::~MyObject()
{
thread.quit();
thread.wait();
}
打印如下:
main thread QThread(0x13169c80)
socket thread QThread(0x13169c80)
MyObject thread QThread(0x28fe1c)
貌似和想象中的不一樣,socket還是在主線程,我們的目標是也要將它移動到新的線程,這里需要注意,socket作為MyObject的成員對象,並不是MyObject的子對象。而moveToThread的作用是更改此對象及其子對象的線程關聯,所以這里並沒有什么毛病。要想socket成為MyObject的子對象也好辦,使用成員指針的方式。
2.3 成員指針的方式
首先修改代碼如下:
#ifndef MYOBJECT_H
#define MYOBJECT_H
#include <QObject>
#include <QThread>
#include <QUdpSocket>
class MyObject : public QObject
{
Q_OBJECT
public:
explicit MyObject(QObject *parent = 0);
~MyObject();
private:
QThread thread;
QUdpSocket *socket = nullptr;
};
#endif // MYOBJECT_H
#include "MyObject.h"
#include <QDebug>
MyObject::MyObject(QObject *parent) : QObject(parent)
{
qDebug() << "main thread" << QThread::currentThread();
socket = new QUdpSocket(this);
this->moveToThread(&thread);
thread.start();
qDebug() << "socket thread" << socket->thread();
qDebug() << "MyObject thread" << this->QObject::thread();
}
MyObject::~MyObject()
{
thread.quit();
thread.wait();
}
打印如下:
main thread QThread(0x13279c80)
socket thread QThread(0x28fe20)
MyObject thread QThread(0x28fe20)
現在socket作為MyObject的子對象,成功移動到新的線程了,這里應該很好理解,socket在構造時指定了this(也就是MyObject)作為父對象。
3 繼續找坑
socket調用下bind,代碼如下:
MyObject::MyObject(QObject *parent) : QObject(parent)
{
qDebug() << "main thread" << QThread::currentThread();
socket = new QUdpSocket(this);
this->moveToThread(&thread);
thread.start();
socket->bind(QHostAddress::Any, 10001);
qDebug() << "socket thread" << socket->thread();
qDebug() << "MyObject thread" << this->QObject::thread();
}
輸出如下:
main thread QThread(0x979c80)
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QUdpSocket(0x14d95e98), parent's thread is QThread(0x28fe20), current thread is QThread(0x979c80)
socket thread QThread(0x28fe20)
MyObject thread QThread(0x28fe20)
這里也很好理解,經過moveToThread后,socket已經移動到新的線程中了,然而MyObject的構造函數是在主線程中執行的,也就是在主線程中調用了屬於另外一個線程的socket的bind函數,bind函數中實例了對象並指定了socket為父對象,也就是在主線程中定義了一個對象,並指定了在另外一個線程的對象為父對象,這樣是不對的,怎么辦呢?在moveToThread之前bind好就可以了。
修改代碼:
MyObject::MyObject(QObject *parent) : QObject(parent)
{
qDebug() << "main thread" << QThread::currentThread();
socket = new QUdpSocket(this);
socket->bind(QHostAddress::Any, 10001);
this->moveToThread(&thread);
thread.start();
qDebug() << "socket thread" << socket->thread();
qDebug() << "MyObject thread" << this->QObject::thread();
}
輸出如下:
main thread QThread(0x13339c80)
socket thread QThread(0x28fe20)
MyObject thread QThread(0x28fe20)
好了,一切正常,聰明的你應該已經知道原因了吧。
4 總結
QObject::moveToThread的作用是更改此對象及其子對象的線程關聯;注意是子對象,並不是成員對象,理解了這個點也就抓住了重點。當然一般做法是在實例對象的地方使用moveToThread,上面的例子是放在了構造函數里面,這樣有個好處,對象實例化出來自動就在新的線程中執行了,MyObject構造函數中使用信號槽與socket通信,同時在MyObject外部也使用信號槽的方式進行通信(不能直接調用函數接口,那樣還是會在主線程中執行),這樣就達到我們的目標了,比起繼承QThread重寫run函數的方式,這確實要簡單多了。