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函数的方式,这确实要简单多了。

© 版权声明
文章版权归作者所有,未经允许请勿转载。【QQ交流:115124903】
THE END
原文地址:https://www.cnblogs.com/luoxiang/p/15343568.html