QT多线程以及事件循环

使用Qt框架有有一段时间了,Qt的多线程总给我一种好用又不好用的感觉,所以写下这篇文章,来总结一下Qt多线程正确的使用方式。

在Qt文档中,QThread 使用方式主要有两种,一种是继承QThread 并重写 QThread::run() 函数,这种方式不做过多赘述,我们主要研究QThread 的第二种使用方法,下面是源于Qt文档的QThread示例。

c++ 复制代码
 class Worker : public QObject
 {
     Q_OBJECT

 public slots:
     void doWork(const QString &parameter) {
         QString result;
         /* ... here is the expensive or blocking operation ... */
         emit resultReady(result);
     }

 signals:
     void resultReady(const QString &result);
 };

 class Controller : public QObject
 {
     Q_OBJECT
     QThread workerThread;
 public:
     Controller() {
         Worker *worker = new Worker;
         worker->moveToThread(&workerThread);
         connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
         connect(this, &Controller::operate, worker, &Worker::doWork);
         connect(worker, &Worker::resultReady, this, &Controller::handleResults);
         workerThread.start();
     }
     ~Controller() {
         workerThread.quit();
         workerThread.wait();
     }
 public slots:
     void handleResults(const QString &);
 signals:
     void operate(const QString &);
 };

代码很简单,首先我们先声明一个继承于QObject 的工作类Worker ,然后在控制类Controller 中声明一个QThreadWorker ,调用WorkerQObject::moveToThread(QThread*) 方法,再开启线程。 这里我们需要注意,在开启线程后,我们不能直接调用Worker的函数,而是必须要通过信号槽的方式去调用,调用函数返回的参数也需要信号槽去传递。

在解释原因之前我们先补充一个概念,叫事件循环,还记的我们在启动Qt程序后,都需要在 main() 最后调用 QCoreApplication::exec() 函数,这个其实就是启动了Qt的事件循环。熟悉libevent或者JavaScript的朋友可能对事件循环比较熟悉了,简单来讲就是我们通过回调函数来向框架注册我们想要处理的事件,当事件产生时,框架会自动运行我们注册的回调函数,事件可以有很多种,例如JavaScript向后端发送http请求返回结果的事件。若想深入了解事件循环,我这里推荐一篇文章。

Node.js源码解析:深入Libuv理解事件循环 - 知乎

这篇文档可以帮助我们理解JavaScript的异步机制以及底层事件循环。

了解完事件循环后,我们可以得知,Qt的 connect() 函数其实就是向Qt框架注册事件回调函数,整个信号槽的执行过程其实就是一个异步执行的过程,从原理上讲Qt的 connect() 与JavaScript的异步没什么区别。

那如何通过Qt的 connect() 实现类似于JavaScript的异步效果,这就需要设置 connect() 的第五个参数。

c++ 复制代码
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)

我们先看看Qt::ConnectionType的选项有哪些

c++ 复制代码
/*
* (Default) If the receiver lives in the thread that emits the signal, Qt::DirectConnection is 
* used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the 
* signal is emitted.
*/
Qt::AutoConnection
/*
* The slot is invoked immediately when the signal is emitted. 
* The slot is executed in the signalling thread.
*/
Qt::DirectConnection
/*
* The slot is invoked when control returns to the event loop of the receiver's thread.
* The slot is executed in the receiver's thread
*/
Qt::QueuedConnection
/*
* Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns.
* This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock.
*/
Qt::BlockingQueuedConnection
/*
* This is a flag that can be combined with any one of the above connection types, using a bitwise OR.
* When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists 
*/
Qt::UniqueConnection

根据文档可知,Qt::AutoConnection 为默认参数,Qt会根据信号发送者和接收者所属的线程自动选择Qt::DirectConnectionQt::QueuedConnection ,说白了当信号发送者和接收者所属同一线程时,选择 Qt::DirectConnection ,当不属于同一线程时 Qt::QueuedConnection

这里我们看一下代码。

c++ 复制代码
class Worker : public QObject
{
    Q_OBJECT
public:
    explicit Worker(QObject *parent = nullptr);
public slots:
    void doWork(const QString &parameter) {
        QString result;
        /* ... here is the expensive or blocking operation ... */
        qDebug()<<parameter;
        emit resultReady(result);
    }
signals:
    void resultReady(const QString &result);
};

class Controller : public QObject
{
    Q_OBJECT
    QThread workerThread;
public:
    Controller(QObject *parent = nullptr):QObject(parent) {
        Worker *worker = new Worker;
//        worker->moveToThread(&workerThread);
//        connect(&workerThread, &QThread::finished, worker, &QObject::deleteLater);
        connect(this, &Controller::operate, worker, &Worker::doWork,Qt::QueuedConnection);
//        connect(worker, &Worker::resultReady, this, &Controller::handleResults);
//        workerThread.start();
    }
    ~Controller() {
        workerThread.quit();
        workerThread.wait();
    }
    void doWork(){
        emit operate("hello world");
    }
public slots:
    void handleResults(const QString &){

    }
signals:
    void operate(const QString &);

};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Controller controller;
    controller.doWork();
    qDebug()<<"do work done";
    return a.exec();
}

这里我们先不使用多线程,只用单线程,但 connect() 函数设置为 Qt::QueuedConnection,输出结果为

c++ 复制代码
do work done
hello world

从结果上可以看出,我们虽然先执行的dowork() ,但hello world是在最后输出的。我们再来试试把参数设置为 Qt::DirectConnection

c++ 复制代码
hello world
do work done

这时输出的顺序与我们的预期是一致的。为什么会出现这种情况,这是因为Qt在接收到 Qt::QueuedConnection 类型的信号时,是不会直接去执行槽函数的,而是把这个信号放到此线程的事件队列中,等待当前任务完成后,再去处理队列中的事件,这是一种异步行为,可能有些抽象,这需要对事件循环机制有一定的了解。

说了那么多事件循环,再回到多线程的问题,我们就能很轻易的理解Qt的线程为什么会用 connect() 函数进行通信,Qt会对每个线程设置单独的事件循环,两者相互独立,两个线程相互通信,实际上就是给对方的事件队列发送事件,整个多线程通信实际上全是异步行为。

相关推荐
SccTsAxR5 分钟前
算法基石:手撕离散化、递归与分治
c++·经验分享·笔记·算法
Q741_14730 分钟前
每日一题 力扣 3655. 区间乘法查询后的异或 II 模拟 分治 乘法差分法 快速幂 C++ 题解
c++·算法·leetcode·模拟·快速幂·分治·差分法
夏乌_Wx35 分钟前
剑指offer | 2.4数据结构相关题目
数据结构·c++·算法·剑指offer·c/c++
米啦啦.37 分钟前
C+类的友元与静态成员函数,类模板
c++·友元·类模板
超绝振刀怪42 分钟前
【C++可变模板参数】
开发语言·c++·可变模板参数
minji...1 小时前
Linux 线程同步与互斥(二) 线程同步,条件变量,pthread_cond_init/wait/signal/broadcast
linux·运维·开发语言·jvm·数据结构·c++
梓䈑1 小时前
高性能 C++ 日志实战:spdlog 核心架构解析与最佳实践指南
c++·架构
草莓熊Lotso2 小时前
【Linux 线程进阶】进程 vs 线程资源划分 + 线程控制全详解
java·linux·运维·服务器·数据库·c++·mysql
唐樽2 小时前
C++ 竞赛学习路线笔记
c++·笔记·学习
ShineWinsu2 小时前
对于Linux:文件操作以及文件IO的解析
linux·c++·面试·笔试·io·shell·文件操作