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函數的方式,這確實要簡單多了。

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