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

相关推荐
QuantumStack2 小时前
【C++ 真题】P1104 生日
开发语言·c++·算法
天若有情6732 小时前
01_软件卓越之道:功能性与需求满足
c++·软件工程·软件
whoarethenext2 小时前
使用 C++/OpenCV 和 MFCC 构建双重认证智能门禁系统
开发语言·c++·opencv·mfcc
Jay_5153 小时前
C++多态与虚函数详解:从入门到精通
开发语言·c++
追风赶月、4 小时前
【QT】事件(鼠标、按键、定时器、窗口)
qt
xiaolang_8616_wjl4 小时前
c++文字游戏_闯关打怪
开发语言·数据结构·c++·算法·c++20
FrostedLotus·霜莲5 小时前
C++主流编辑器特点比较
开发语言·c++·编辑器
牵牛老人7 小时前
Qt处理USB摄像头开发说明与QtMultimedia与V4L2融合应用
stm32·单片机·qt
liulilittle9 小时前
深度剖析:OPENPPP2 libtcpip 实现原理与架构设计
开发语言·网络·c++·tcp/ip·智能路由器·tcp·通信
十年编程老舅10 小时前
跨越十年的C++演进:C++20新特性全解析
c++·c++11·c++20·c++14·c++23·c++17·c++新特性