使用Qt框架有有一段时间了,Qt的多线程总给我一种好用又不好用的感觉,所以写下这篇文章,来总结一下Qt多线程正确的使用方式。
在Qt文档中,QThread 使用方式主要有两种,一种是继承QThread 并重写 QThread::run() 函数,这种方式不做过多赘述,我们主要研究QThread 的第二种使用方法,下面是源于Qt文档的QThread示例。
c++
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork(const QString ¶meter) {
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 中声明一个QThread 和Worker ,调用Worker 的 QObject::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::DirectConnection 或 Qt::QueuedConnection ,说白了当信号发送者和接收者所属同一线程时,选择 Qt::DirectConnection ,当不属于同一线程时 Qt::QueuedConnection
这里我们看一下代码。
c++
class Worker : public QObject
{
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr);
public slots:
void doWork(const QString ¶meter) {
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会对每个线程设置单独的事件循环,两者相互独立,两个线程相互通信,实际上就是给对方的事件队列发送事件,整个多线程通信实际上全是异步行为。