Qt 中的多线程 02:移动工作对象到子线程中

Qt 中的多线程除了通过继承 QThread 类,然后重写虚函数 run() 之外还有另一种方案:将要执行的复杂操作,放在一个继承自 QObject 的类中,然后使用 QObject::moveToThread(QThread*) 函数将工作对象的移动到创建的子线程中去执行。

一、子线程的实现

工作对象类

该类继承自 QObject 主要是用来实现一些耗时复杂的操作,这些操作可能会阻塞主线程,因此需要在后面将其移到子线程中去执行。

1、MyWork.h 文件

#pragma once

#include <QObject>

class MyWork: public QObject
{
    Q_OBJECT
public:
    MyWork();
    ~MyWork();

signals:

    /**
     * 子线程用来向主线程发送信号.
     */
    void  SendMsg(QString msg);

public slots:

    /**
     * 复杂操作在槽函数中实现,然后通过信号槽来与主线程建立联系
     * 以触发该复杂的槽函数执行。
     */
    void DoSomething();

};

2、MyWork.cpp 文件

#include "MyWork.h"
#include <QThread>

MyWork::MyWork()
{

}

MyWork::~MyWork()
{

}

void MyWork::DoSomething()
{
    SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
    SendMsg("Begin Work ......");

    QThread::sleep(6);

    SendMsg("Work Finished.");
}

主线程类

将在主线程类中去创建子线程,和 工作对象,并将工作对象移动到子线程中,然后通过主程序的信号来触发位于子线程中的工作对象,让其执行一些复杂的操作。

1、QThreadTest.h 文件

#pragma once

#include <QtWidgets/QMainWindow>
#include "ui_QThreadTest.h"
#include <QThread>

#include "MyWork.h"

class QThreadTest : public QMainWindow
{
    Q_OBJECT

public:
    QThreadTest(QWidget *parent = Q_NULLPTR);
    ~QThreadTest();

signals:

    /**
     *  主线程通过触发信号,来让子线程开始执行工作.
     */
    void BeginWork();

private slots:
    /**
     * 直接在主线程中进行耗时操作,会直接阻塞主UI线程,导致程序卡死.
     */
    void OnBtn01Clicked();

    /**
    * @brief  通过点击Button来触发对应的信号,然位于子线程中的工作对象开始执行对应的函数
    */
    void OnBtn02Clicked();

    /**
    * @brief  由于子线程不能直接操作UI,所以该函数用于将接受到的子线程内容显示在UI上
    * @param  [in]  msg
    */
    void GetWorkMsg(QString msg);

private:
    Ui::QThreadTestClass ui;
    QThread *subThread;     // 创建子线程,用来将任务移动到该线程中去执行
};

2、QThreadTest.cpp 文件

#include "QThreadTest.h"
#include <QPushButton>

QThreadTest::QThreadTest(QWidget *parent)
    : QMainWindow(parent)
{
    ui.setupUi(this);

    subThread = new QThread(this);

    // 创建工作对象,不能为其指定父指针,否则后面无法将其移动子线程中去执行
    MyWork* myWork = new MyWork();

    // 将工作对象移动到子线程中去执行
    myWork->moveToThread(subThread);

    // 在子线程执行结束之后,将工作对象的资源进行释放
    connect(subThread, &QThread::finished, myWork, &MyWork::deleteLater);

    // 通过主线程的信号来触发子线程开始执行对应的工作
    connect(this, &QThreadTest::BeginWork, myWork, &MyWork::DoSomething);

    // 在子线程中的工作对象无法直接修改主线程的UI,因此需要通过信号槽的方式来进行更新
    connect(myWork, &MyWork::SendMsg, this, &QThreadTest::GetWorkMsg);

    // 启动子线程,此时只是子线程开始执行,但是子线程中的具体工作 DoSomething 还未执行
    subThread->start();


    connect(ui.btn01, &QPushButton::clicked, this, &QThreadTest::OnBtn01Clicked);
    connect(ui.btn02, &QPushButton::clicked, this, &QThreadTest::OnBtn02Clicked);
}

QThreadTest::~QThreadTest()
{
    // 创建的线程一定要进行释放,否则关闭程序的时候线程仍然在运行,此时会造成程序Crash
    subThread->quit();
    subThread->wait();
}

void QThreadTest::OnBtn01Clicked()
{
    ui.textBrowser_01->append("Start do something ...");

    QThread::sleep(5);

    ui.textBrowser_01->append("Finishing this operation.");
}

void QThreadTest::OnBtn02Clicked()
{
    ui.textBrowser_02->append(QString("主线程id = %1").arg((int)QThread::currentThreadId()));

    emit BeginWork(); // 触发信号,子线程中开始执行工作对象的函数
}

void QThreadTest::GetWorkMsg(QString msg)
{
    ui.textBrowser_02->append(msg);
}


该方法的主要流程为:

  1. 创建一个类,如 MyWork 继承自 QObject ,后续会将所有复杂耗时操作都放到该类的槽函数中。
  2. MyWork 类中将复杂的操作放在槽函数中,后续会通过信号槽的方式来触发子线程执行该操作。
  3. MyWork 类中定义信号槽,用于子线程向主线程更新数据或信息。
  4. 在主线程中创建 QThread 对象,和 MyWork类对象,并将 工作对象移动到子线程中。
  5. 分别建立信号槽:
    • 子线程执行完工作后,释放工作对象。(此时子线程可能并未退出)
    • 主线程的开始执行信号连接工作对象的槽函数。
    • 子线程向主线程发送数据,用来更新UI或同步操作。
  6. 启动子线程,此时工作对象的复杂操作还未执行。
  7. 在主线程中某一时机下,触发开始在子线程中执行工作类中的槽函数,此时才会进行复杂操作的执行。

注意
此时阻塞发生在子线程中,而不是主线程中。因此一般一个工作类中可以有多个耗时的操作,这些耗时的操作都会作为槽函数,然后通过信号槽的方式在主线程中去触发该耗时操作。但是在同一个时刻应该只执行一个耗时的操作,如果有多个耗时的操作同时执行,仍然会发生阻塞,只是阻塞在子线程中。后触发的操作需要等待先触发的操作执行完成之后才能进行。所以可以将多个耗时的操作分别放在不同的继承自 QObject 的工作类中,以便后续将其分别放到不同的子线程中执行,从而实现并行的操作。


二、关于MyWork类的变量、函数以及对象所在的线程

虽然在主线程中将 MyWork 类的对象通过 myWork->moveToThread(subThread) 函数移动到子线程中,但并不是所有的变量、函数或操作都会在子线程中执行,其中一部分的操作仍然位于主线程。

  • QThreadTest 类的所有对象和成员都是属于主线程,其操作也都是在主线程中。
  • MyWork 类中只有通过信号槽来触发的槽函数是在子线程中执行的,其它的函数仍在主线程中执行
  1. MyWork 构造函数中的操作位于 主线程
MyWork::MyWork()
{
    //   在构造函数中打印当前所在线程的 id
    qDebug() << QString(" MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
}

以上代码执行结果如下所示: 构造函数的线程 ID 和 主线程ID相同,所以在 MyWork 类的构造函数中所初始化的变量或操作都属于 主线程,而非 子线程

  1. 槽函数 DoSomething() 中的操作位于 子线程 中,该函数中初始化的变量也属于子线程

三、子线程中 QTimer 功能

因为 Qt 中事件循环默认在主线程中执行,而子线程中如果没有 exec() ,则会执行之后直接退出,也无法响应子线程的事件循环。
如果在子线程中使用 QTimer 需要注意 QTimer 对象的实例化是否在子线程中进行的,如果是在子线程中进行实例化,则QTimer 对象属于子线程,此时可以在子线程中操作计时器,即调用 QTimer->start(),否则将无法在子线程中来启动计时器

1. 在子线程中进行实例化 QTimer

如下子线程中调用 QTimer代码:
MyWork.h 文件

#pragma once

#include <QObject>
#include <QTimer>

class MyWork: public QObject
{
    Q_OBJECT
public:
    MyWork();
    ~MyWork();

signals:

    /**
     * 子线程用来向主线程发送信号.
     */
    void  SendMsg(QString msg);

public slots:

    /**
     * 复杂操作在槽函数中实现,然后通过信号槽来与主线程建立联系
     * 以触发该复杂的槽函数执行。
     */
    void DoSomething();

    /**
    * @brief  计时器被触发的动作
    */
    void RespondTimeOut();

private:
    QTimer* m_timer;  // 该计时器指针在子线程中进行实例化,则属于子线程;在主线程中实例化,则属于主线程
};

MyWork.cpp 文件

#include "MyWork.h"
#include <QThread>
#include <QDebug>

MyWork::MyWork()
{
    qDebug() << QString(" MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());
}

MyWork::~MyWork()
{
    // 释放 QTimer 对象的资源
    m_timer->stop();
    delete m_timer;
    qDebug() << "Destructor MyWork....";
}

void MyWork::DoSomething()
{
    SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
    SendMsg("Begin Work ......");

    QThread::sleep(3);

    SendMsg("Work Finished.");

    // 在子线程的函数中实例化,则该对象属于子线程,此时可以在子线程中启动计时器:m_timer->start()
    m_timer = new QTimer();
    m_timer->setInterval(1000);
    connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
    m_timer->start();
}

void MyWork::RespondTimeOut()
{
    qDebug() << QString(" Timer thread id = %1").arg((int)QThread::currentThreadId());
}

执行结果如下所示:当前计时器响应槽函数所在的线程ID 与 子线程ID相同。

2. 不在子线程中实例化 QTimer

如果将实例化 QTimer的操作放在 MyWork 的构造函数中,且不指定其父对象,此时 QTimer的对象将属于主线程,子线程函数中将无法操作该计时器对象。

MyWork::MyWork()
{
    qDebug() << QString("MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());

    // 在构造函数中实例化 QTimer ,且不指定其父对象,此时计时器对象属于主线程。
    m_timer = new QTimer();
    m_timer->setInterval(1000);
    connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
}

void MyWork::DoSomething()
{
    SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
    SendMsg("Begin Work ......");

    QThread::sleep(3);

    SendMsg("Work Finished.");

    // 此时在子线程函数中启动计时器,将提示无效。
    m_timer->start();
}

运行结果如下所示,提示无法从另一个线程中启动计时器。因为此时计时器对象属于主线程,而 DoSomething() 函数操作位于子线程,所以无法从子线程中来启动主线程的计时器。

此时如果指定其父对象为 this,即当前类MyWork对象的指针,由于该类通过 moveToThread 移动到子线程中,此时 QTimer 对象 m_timer 也属于子线程,因此可以在子线程函数中来操作该计时器,并使其生效。

MyWork::MyWork()
{
    qDebug() << QString("MyWork constructor thread id = %1").arg((int)QThread::currentThreadId());

    // 在构造函数中实例化 QTimer ,并指定其父对象为当前类对象
    m_timer = new QTimer(this);
    m_timer->setInterval(1000);
    connect(m_timer, &QTimer::timeout, this, &MyWork::RespondTimeOut);
}

void MyWork::DoSomething()
{
    SendMsg(QString("子线程id = %1").arg((int)QThread::currentThreadId()));
    SendMsg("Begin Work ......");

    QThread::sleep(3);

    SendMsg("Work Finished.");

    // 此时在子线程函数中可以启动计时器
    m_timer->start();
}

运行结果如下:

posted @ 2023-12-27 16:33  Jeffxue  阅读(388)  评论(0编辑  收藏  举报