对于界面开发而言,多线程一个非常重要的作用就是将复杂的运算处理分开执行,避免造成界面的卡顿和冻结,甚至被系统强制关闭。另外,使用多线程编程可以最大限度地调用CPU资源,尤其对于多处理器系统。本文基于Visual Studio 2015、Qt5.6.3的项目,整理了多种Qt多线程技术的实例和介绍,并简单归纳了使用场景和注意事项。
1 QThread
继承QThread,重载run函数。
QThread是Qt提供的类,比较适用于处理耗时很长的业务。要创建一个新的线程,可以定义一个MyThread类继承自QThread,然后重新实现QThread::run()。我们可以通过信号和槽机制来通知主线程,比如线程结束消息。
示例代码:
.h
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QThread>
class MyThread : public QThread
{
Q_OBJECT
public:
explicit MyThread(QObject *parent = 0);
~MyThread();
protected:
void run();
signals:
/* 处理完成信号 */
void isDone();
};
#endif // MYTHREAD_H
.cpp
#include "MyThread.h"
MyThread::MyThread(QObject *parent)
{
}
MyThread::~MyThread()
{
}
void MyThread::run()
{
/* 处理耗时操作 */
sleep(5);
/* 发送完成信号 */
emit isDone();
}
使用方法:
使用start()来启动线程
MyThread *pThread = new MyThread(this);
connect(pThread , &MyThread::isDone, this, &MyWidget::slotDone);
connect(pThread , &MyThread::finished, pThread , &QObject::deleteLater);
使用quit()、wait()来关闭线程
/* 停止线程 */
thread->quit();
/* 等待线程处理完手头工作 */
thread->wait();
注意事项:
1)run()函数在新线程中执行,run()函数执行结束,线程结束。
2)MyThread实例化的对象属于创建它的线程,而不是run()函数所在线程。
3)MyThread没有事件循环,除非在run()函数中调用exec();
4)队列连接到MyThread的slotDone()函数,slotDone()函数在创建MyThread对象的线程中执行。
5)直接调用MyThread的方法,该方法的执行线程为调用处的线程。
2 QObject
继承QObject,调用void QObject::moveToThread(QThread *targetThread)。
这种方法适用于在一个类中处理多个耗时任务,且这个些任务不会并行执行的情况。
示例代码:
#ifndef MYTHREAD_H
#define MYTHREAD_H
#include <QObject>
class MyThread : public QObject
{
Q_OBJECT
signals:
/* 处理完成信号 */
void isDone();
public slots:
void doWork()
{
/* TODO:处理耗时操作 */
/* 发送完成信号 */
emit isDone();
}
};
#endif // MYTHREAD_H
使用方法:
创建一个Qthread子线程对象,把我们的自定义线程类,加入到子线程;启动子线程,只是把线程开启了,并没有启动线程处理函数。
QThread *pThread = new QThread(this);
MyThread *pMyThread= new MyThread ;
pMyThread->moveToThread(pThread);
connect(pThread , &QThread::finished, pMyThread, &QObject::deleteLater);
connect(this, &MyWidget::StartWork, pMyThread, &MyThread::doWork);
connect(pMyThread, &MyThread::isDone, this, &MyWidget::slotDone);
pThread.start();
通过关闭子线程来关闭自定义线程类
pThread.quit();
pThread.wait();
注意事项:
1)调用moveToThread函数的对象不能设置父对象。
2)MyThread类中的槽函数可以跟任意线程的任意信号建立连接,队列连接时,在新线程中执行。
3)直接调用MyThread类中的函数,在调用线程内执行。
4)同时发送多个与MyThread类中槽函数连接的信号,槽函数依次执行。
3 QThreadPool
频繁创建和销毁线程会带来较大的性能开销,影响程序执行效率。使用QThreadPool可以有效解决这个问题。QThreadPool搭配QRunnable使用非常简单,如下所示。
示例代码:
class MyThread: public QRunnable
{
void run() override
{
qDebug() << "Test MyThread" << QThread::currentThread();
}
};
MyThread* pThread= new MyThread();
/* QThreadPool会自动销毁pThread */
QThreadPool::globalInstance()->start(pThread);
注意事项:
1)默认情况下,run()函数执行完,pThread对象会被线程池自动删除。可以使用setAutoDelete函数设置。
2)QThreadPool::start()多次启动设置为autoDelete的QRunnable对象,可能导致崩溃。
4 QtConcurrent
QtConcurrent提供了高级api,使编写多线程程序时,不需要使用诸如互斥锁、读写锁、等待条件或信号量等低级线程安全类。
4.1 Run()函数
QtCuncurrent::run()表示在一个单独的线程中执行函数,它的基本原型如下:
QFuture QtConcurrent::run(QThreadPool *pool, Function function, …)
参数 pool :线程池。表示从线程池中获取一个线程来执行该函数。
参数 function: 表示要在线程中执行的函数。
返回值 :返回一个 QFuture<T>对象。
实际使用时,常用到QFuture QtConcurrent::run(Function function, …),相当于QtConcurrent::run(QThreadPool::globalInstance(), function, …),表示从全局的线程池中,获取线程。
示例代码:
extern void testFunction(int arg1, double arg2, const QString &arg3);
int iValue = 0;
double dValue = 0;
QString strValue;
QFuture<void> future = QtConcurrent::run(testFunction, iValue, dValue, strValue);
在调用run()的时候,这些值会被传递给执行函数,之后修改这些变量的值, 比如重新设置 iValue的值,并不会对计算产生影响。
另外,function可以为普通函数、类的成员函数、仿函数、lambda表达式,
示例代码:
(1)调用const的成员函数
// call 'QList<QByteArray> QByteArray::split(char sep) const' in a separate thread
QByteArray bytearray = "hello world";
QFuture<QList<QByteArray> > future = QtConcurrent::run(bytearray, &QByteArray::split, ',');
...
QList<QByteArray> result = future.result();
(2)调用non-const成员函数
// call 'void QImage::invertPixels(InvertMode mode)' in a separate thread
QImage image = ...;
QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
...
future.waitForFinished();
// At this point, the pixels in 'image' have been inverted
(3)使用lambda表达式:
QFuture<void> future = QtConcurrent::run([=]() {
// Code in this block will run in another thread
});
...
注意事项:
1)run里面的函数可能不会立即执行;一旦线程池中的线程有可用的线程时,才会被执行。
4.2 QFuture
使用QFuture可以获取和控制当前计算的状态并获取计算完毕之后的结果。
示例代码:
extern int func(void);
int func(void)
{
/* do some thing */
}
QFuture<int> future = QtConcurrent::run(func);
/* 该函数会阻塞等待线程执行func的计算结果返回 */
int result = future.result();
另外,QFuture的常用接口有下面这些:
(1)获取结果相关函数
result()函数会判断结果是否为可用的,如果不可用则阻塞等待。如果可用则直接把结果返回。
(2)设置运行状态函数
cancel() 取消,pause() 暂停,Resumes() 恢复,这些函数在run() 这种模式下无效。
(3)获取运行状态函数
isCanceled() 、 isStarted()、 isFinished()、isRunning()和isPaused()。
(4)进度信息
progressValue()、progressMinimum()、progressMaximum()和progressText() ,在run() 模式下,这些值并没有正真的意义。waitForFinished() 会阻塞等待计算的完成,直到结果为可用的状态。
注意事项:
(1)QFuture<T> 模板 T 这个类型,需要有默认构造和拷贝构造。
(2)QFuture是一个基于引用计数的轻量级的类。
4.3 QFutureWatcher
我们通常想要通过信号和槽的函数,异步的监控QFuture状态信息。这就需要 QFutureWatcher这个类。
示例代码:
// Instantiate the objects and connect to the finished signal.
MyClass myObject;
QFutureWatcher<int> watcher;
connect(&watcher, SIGNAL(finished()), &myObject, SLOT(handleFinished()));
// Start the computation.
QFuture<int> future = QtConcurrent::run(...);
watcher.setFuture(future);
上述代码中,用setFuture设置被监控的QFuture对象,为收到QFuture对象发送的信号,需要在执行run()之前设置信号和槽的连接。当QFuture对象的结果可用的时候,QFutureWatcher会发送finished信号 。
5 使用场景
下面这张表简单整理了上面介绍的4种Qt多线程方法及其应用场景,
生命周期 | 开发任务 | 解决方案 |
---|---|---|
一次调用 | 在另一个线程中运行一个函数,函数完成时退出线程 | (1)编写函数,使用QtConcurrent::run运行它;(2)派生QRunnable,使用QThreadPool::globalInstance()->start() 运行它;(3)派生QThread,重新实现QThread::run() ,使用QThread::start() 运行它 |
一次调用 | 需要操作一个容器中所有的项。使用处理器所有可用的核心。一个常见的例子是从图像列表生成缩略图。 | QtConcurrent 提供了map()函你数来将操作应用到容器中的每一个元素,提供了fitler()函数来选择容器元素,以及指定reduce函数作为选项来组合剩余元素。 |
一次调用 | 一个耗时运行的操作需要放入另一个线程。在处理过程中,状态信息需要发送会GUI线程。 | 使用QThread,重新实现run函数并根据需要发送信号。使用信号槽的queued连接方式将信号连接到GUI线程的槽函数。 |
持久运行 | 生存在另一个线程中的对象,根据要求需要执行不同的任务。这意味着工作线程需要双向的通讯。 | 派生一个QObject对象并实现需要的信号和槽,将对象移动到一个运行有事件循环的线程中并通过queued方式连接的信号槽进行通讯。 |
持久运行 | 生存在另一个线程中的对象,执行诸如轮询端口等重复的任务并与GUI线程通讯。 | 同上,但是在工作线程中使用一个定时器来轮询。尽管如此,处理轮询的最好的解决方案是彻底避免它。有时QSocketNotifer是一个替代。 |
6 替代线程技术
实际用Qt开发时,也有一些其他技术可以代替多线程实现差不多的效果,
替代技术 | 注解 |
---|---|
QEventLoop::processEvents() | 在一个耗时的计算操作中反复调用QEventLoop::processEvents() 可以防止界面的假死。尽管如此,这个方案可伸缩性并不太好,因为该函数可能会被调用地过于频繁或者不够频繁。 |
QTimer | 后台处理操作有时可以方便地使用Timer安排在一个在未来的某一时刻执行的槽中来完成。在没有其他事件需要处理时,时间隔为0的定时器超时事件被相应 |
QSocketNotifier、QNetworkAccessManager、QIODevice::readyRead() | 这是一个替代技术,替代有一个或多个线程在慢速网络执行阻塞读的情况。只要响应部分的计算可以快速执行,这种设计比在线程中实现的同步等待更好。与线程相比这种设计更不容易出错且更节能(energy efficient)。在许多情况下也有性能优势。 |
7 开发中的坑
下面补充了几个在实际开发中遇到的几个坑。
7.1 子线程中操作UI
现象:
在子线程中操作UI直接崩溃。
处理办法:
Qt创建的子线程中是不能对UI对象进行任何操作的,即QWidget及其派生类对象。正确的操作应该是通过信号槽,将一些参数传递给主线程,让主线程去处理。
7.2 信号槽连接方式写错
现象:
连接方式使用了直接连接(DirectConnection),主线程收不到异步线程的消息。
处理办法:
在线程间进行信号槽连接时,使用队列连接方式(QueuedConnection)或者自动连接(AutoConnection)。
7.3 退出界面但没有关闭线程
现象:
退出某个子页面后,过了一会儿弹出上个页面的成功or失败提示。
处理办法:
比如Qthread,可以通过quit()、wait()来关闭线程,有时候需要通过改变线程里面循环的判断条件来使之结束。