Qt小技巧9.moveToThread的使用技巧
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函数的方式,这确实要简单多了。
每一步踏出,都是一次探索,一次成长。