Qt多线程的建议实现

Qt使用QThread对象管理线程,通常有两种多线程实现:

  1. 重载QThread::run()函数:非常简单的方法,定义一个继承自QThread类的类,并重写run()函数即可。该方法易于操作,但在操作涉及对象操作、事件循环等时,易出现难以调试的内存错误和/或线程调度错误。同时可能存在大量的线程切换,影响性能。
  2. 定义一个继承自QObject的工作者类,并使用moveToThread()函数将其移动到QThread对象内:使用此方法可以简洁地使用Qt的事件循环机制。通过一系列信号-槽的连接,主线程与子线程分别使用独立的事件循环,并进行安全的通讯。同时工作者类的所有函数实现均可在子线程中进行。但是,此方法要求所有跨线程的互操作全部使用Qt的信号-槽机制完成,需要进行大量的connect()操作以及信号-槽定义。

重载QThread::run()函数

该方法的实现较为简单,此处假设类定义位于头文件ChildThread.h中,实现于代码文件ChildThread.cpp中。

头文件ChildThread.h

#ifndef CHILDTHREAD_H
#define CHILDTHREAD_H
 
#include <QThread>
 
class ChildThread : public QObject {
    Q_OBJECT
 
public:
    ChildThread();
    virtual ~ChildThread();
 
protected:
    void run(); //重载执行函数
};
 
#endif // CHILDTHREAD_H

代码文件ChildThread.cpp

#include "ChildThread.h"
 
void ChildThread::run() {
    /* 在这里编写异步工作 */
    return;
}

使用

使用时,实例化ChildThread,并执行ChildThread::start()即可。

说明

重载QThread::run()函数的方法适合大多数简单的操作,但是不适合需要Qt事件循环介入的操作。这种操作可能带来非预期的线程切换乃至内存错误(例如在高速执行时的堆栈损坏错误)。

moveToThread()方法

在目前常见Qt版本的QThread::run()默认实现中,会调用QThread::exec()函数启动线程自身的事件循环,因此,可以结合Qt自身的信号-槽事件循环机制,将整个工作者类移动到QThread对象内,并使用信号和槽的绑定,异步地启动相应工作。

这样的实现通常包括两个继承自QObject的对象:

  1. 一个Worker工作者对象:这个对象定义了一系列槽函数,用于异步地执行相应的工作。
  2. 一个Controller控制器对象:这个对象包含了一个QThread线程,以及Worker工作者对象的实例。该线程同时定义了一系列信号函数,用于启动Worker工作者对象中的槽函数。

工作者对象

假设工作者对象定义位于头文件Worker.h中,实现于代码文件Worker.cpp中。

头文件Worker.h

#ifndef WORKER_H
#define WORKER_H
 
#include <QObject>
 
class Worker : public QObject {
    Q_OBJECT
 
public:
    Worker();
    ~Worker();
 
public slots:
    /* 这里放置各类槽函数的实现 */
    void ExecuteWorkRequestedEventHandler();
};
 
#endif // WORKER_H

代码文件Worker.cpp

#include "Worker.h"
 
Worker::Worker() {
}
 
Worker::~Worker() {
}
 
void Worker::ExecuteWorkRequestedEventHandler() {
    /* 在这里编写异步工作 */
    return;
}

控制器对象

假设控制器对象定义位于头文件Controller.h中,实现于代码文件Controller.cpp中。

头文件Controller.h

#ifndef CONTROLLER_H
#define CONTROLLER_H
 
#include <QObject>
#include <QThread>
 
class Controller : public QObject {
    Q_OBJECT
 
public:
    /* 构造与析构 */
    Controller();
    ~Controller();
 
    /* 操作接口 */
    void ExecuteWork();
 
signals:
    /* 这里放置各类用于与工作者通讯的信号 */
    void ExecuteWorkRequestedEvent();
 
private:
    /* 子线程对象 */
    QThread * trdWorkerThread;
 
    /* 工作者对象 */
    Worker * wrkWorker;
};
 
#endif // CONTROLLER_H

代码文件Controller.cpp

#include "Controller.h"
 
/* 构造与析构 */
Controller::Controller() {
    //实例化工作者对象
    wrkWorker = new Worker;
 
    //实例化子线程对象
    trdWorkerThread = new QThread;
 
    //将工作者对象移动到子线程
    wrkWorker->moveToThread(trdWorkerThread);
 
    //连接信号与槽
    connect(this, SIGNAL(ExecuteWorkRequestedEvent()), wrkWorker, SLOT(ExecuteWorkRequestedEventHandler)); //对于信号发射者和接收者不在同一线程的情况,会默认使用Qt::QueuedConnection队列化连接,这样信号即可在触发后被置入目标线程的消息队列(事件循环)中
    //如果工作者对象wrkWorker有子对象需要进行信号-槽的连接,也可以在这里使用Qt::QueuedConnection方式连接:
    //connect(wrkWorker->objSubObject, SIGNAL(EventOfSubObject()), wrkWorker, SLOT(EventOfSubObjectHandler()));
 
    //启动子线程的事件循环
    trdWorkerThread->start();
}
 
Controller::~Controller() {
    //结束子线程的事件循环
    trdWorkerThread->quit();
    trdWorkerThread->wait();
 
    //删除工作者对象
    wrkWorker->deleteLater();
    wrkWorker = NULL;
 
    //删除子线程对象
    trdWorkerThread->deleteLater();
    trdWorkerThread = NULL;
}
 
/* 操作接口 */
void Controller::ExecuteWork() {
    emit ExecuteWorkRequestedEvent(); //发射相应的信号到目标进程的消息队列
    return;
}

使用

使用时,实例化Controller,并执行Controller::ExecuteWork()即可。

说明

本方法可以充分利用子线程的事件循环机制,并在子线程中执行工作者对象的的几乎所有函数。

但需要注意的是,必须通过Qt的信号-槽机制从主线程启动工作者对象的工作。直接引用相应函数将导致这些函数在主线程内执行。

需要注意的是,在Qt中使用moveToThread()函数将QObject对象移动到另一线程时,若被移动的对象包含了指向其它对象的指针,这些对象不会自动被移动。因此,使用moveToThread()函数将QObject对象移动到另一线程时,需要对目标对象自身及其所有子对象调用moveToThread()函数,否则可能诱发“QObject不能跨线程创建对象”的运行时警告。

删除容器线程时,应先删除容器线程的所有子对象,再删除容器线程,否则将导致错误。

对于需要需要在子线程中执行时长无限的耗时操作的情况,一种常见的实现是使用while (true) {}循环,并结合QThread::sleep()QThread::usleep()实现等待。这种情况下,可能在运行中遇到意料之外的线程阻塞异常,甚至可能导致整个程序(包括主线程和子线程)进入休眠状态。这时,一个比较推荐的行为是使用QTimer::singleShot()函数,通过0毫秒的延时值模拟无限循环的行为。

参考资料

https://doc.qt.io/archives/qt-4.8/qthread.html

https://zhuanlan.zhihu.com/p/349896858

it
除非特别注明,本页内容采用以下授权方式: Creative Commons Attribution-ShareAlike 3.0 License