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会对每个线程设置单独的事件循环,两者相互独立,两个线程相互通信,实际上就是给对方的事件队列发送事件,整个多线程通信实际上全是异步行为。

相关推荐
KyollBM6 分钟前
【CF】Day75——CF (Div. 2) B (数学 + 贪心) + CF 882 (Div. 2) C (01Trie | 区间最大异或和)
c语言·c++·算法
feiyangqingyun18 分钟前
Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动
c++·qt·udp·gb28181
CV点灯大师20 分钟前
C++算法训练营 Day10 栈与队列(1)
c++·redis·算法
jllws11 小时前
Qt学习及使用_第1部分_认识Qt---学习目的及技术准备
qt·c++框架
成工小白1 小时前
【C++ 】智能指针:内存管理的 “自动导航仪”
开发语言·c++·智能指针
sc写算法1 小时前
基于nlohmann/json 实现 从C++对象转换成JSON数据格式
开发语言·c++·json
SunkingYang1 小时前
C++中如何遍历map?
c++·stl·map·遍历·方法
Andrew_Xzw2 小时前
数据结构与算法(快速基础C++版)
开发语言·数据结构·c++·python·深度学习·算法
库库的里昂2 小时前
【C++从练气到飞升】03---构造函数和析构函数
开发语言·c++
到点就困告2 小时前
海康工业相机SDK二次开发(VS+QT+海康SDK+C++)
数码相机·qt·海康