简述

Qter们经常遇到由于耗时操作造成GUI阻塞的问题。其实,这个问题并不难克服,可以采用许多不同的方式,下面我会列举一些可选范围,根据使用情况进行处理。

执行耗时操作

我们需要做的第一件事就是确定能够解决问题的范围。上述问题可能会以两种形式出现。

  1. 当一个程序执行计算密集型的任务,为了获得一系列按顺序操作的最终结果。这样任务的一个例子是:计算一个快速傅立叶变换。

  2. 当一个程序触发一些行为(例如:网络下载),等待它完成之后,继续算法的下一个步骤。使用Qt时,这个问题很容易避免,因为大部分框架执行的异步任务完成以后都会发出相应的信号,可以把它连接到槽函数上延续算法。

在计算期间(忽略任何信号槽使用)所有事件处理会停止。其结果是:GUI未刷新、用户输入不处理、网络活动停止 - 应用程序看起来被阻塞了。实际上,不相关的部分耗时任务阻塞了,多长时间属于“耗时操作”? - 任何一切干扰用户和程序交互的都算。一秒比较长,所有超过两秒以上的绝对太长了。

这节,我们的目标是保证功能,同时防止用户界面被阻塞而惹恼用户。要做到这一点,来看看解决方案。有两种方式可以达到执行计算的最终目标:

  1. 在主线程计算(单线程方式)
  2. 单独的线程(多线程方式)

后者广为人知,但它有时被滥用。因为有时一个线程处理完全可以。与流行观点相反,线程通常会减缓你的应用程序而不是加速。所以,除非你确信程序可以在多线程中受益(无论对效率还是简单而言),尽量避免产生新的线程,因为你可以。

手动事件处理

最基本的解决方案是明确要求Qt在计算的某些时刻处理等待事件。要做到这一点,必须定期调用QCoreApplication::processEvents()。

下面的例子显示如何做到这一点:

for (int i = 3; i <= sqrt(x) && isPrime; i += 2) {
label->setText(tr("Checking %1...").arg(i));
if (x % i == 0)
isPrime = false;
QCoreApplication::processEvents();
if (!pushButton->isChecked()) {
label->setText(tr("Aborted"));
return;
}
}

这种方法有明显的缺点。例如:假设你想要并行调用两个类似这样的循环,其中一个将会阻止另一个直到第一个完成(所以你不能分配计算能力到不同的任务)。这也会使应用程序的事件延迟反应。此外,代码很难阅读和分析,因此这种方法只适合单线程中处理的简短的操作,如:启动画面和短期监控操作。

使用一个工作线程

另一个避免阻塞主事件循环的解决方案:在一个单独的线程中处理耗时操作。如果任务由第三方库以阻塞方式执行,这就显得非常有用。这种情况下,可能不能中断它让GUI处理等待事件。

在单独的线程中执行操作的一种方法是使用QThread。可以子类化QThread并实现run()函数,或调用QThread::exec()启动线程的事件循环,当然,也可以两者兼有。然后可以用信号槽的方式与主线程通信。切记:必须确保使用Qt::QueuedConnection类型连接,否则线程可能会失去稳定性,导致程序crash。

在Qt参考文档和在线资料里有很多使用线程的示例,所以这里就不自己实现了,主要专注于其它有趣的方面。

等待本地事件循环

接下来的解决方案,我想描述处理等待直到异步任务完成。这里,我会告诉你如何阻塞流动,直到没有阻塞事件处理的网络操作完成,基本上是这样:

task.start();
while (!task.isFinished())
QCoreApplication::processEvents();

这就是所谓的忙等待 - 不断地检查状态,直到不满足条件。大多数情况下,这是个坏主意 - 往往会吃掉所有的CPU并且具有手动事件处理的所有缺点。

幸运的是,Qt有一个类可以帮助我们完成任务 - QEventLoop:与应用程序和模态对话框使用exec()一样。这个类的每个实例都会连接到主事件调度机制,当exec()函数被调用时,就开始处理事件,直到使用quit()让其退出。

我们可以利用该机制把异步操作转换成同步操作,使用信号和槽 - 可以开启一个本地事件循环,当接收到特定对象的特定信号时,让它退出:

QNetworkAccessManager manager;
QEventLoop loop;
QTimer timer; timer.setSingleShot(true);
connect(&timer, SIGNAL(timeout()), &loop, SLOT(quit()));
connect(&manager, SIGNAL(finished(QNetworkReply*)), &loop, SLOT(quit()));
QNetworkReply *reply = manager.get(QNetworkRequest(QUrl("http://www.qtcentre.org"))); timer.start(5000); // 5s超时
loop.exec(); if (timer.isActive()){
// 下载完成
timer.stop();
} else {
// 超时
}

我们使用了QNetworkAccessManager来获取远程URL,因为它是异步的,我们创建一个本地事件循环等待finished()信号。此外,还创建了一个超时为5s的定时器,万一出错将终止事件循环。调用get()和start()发送请求并启动定时器,进入事件循环。在下载完成或5s超时时间到了之后(这取决谁先完成),exec()就会返回。我们通过检测定时器是否处于激活状态,来判断究竟是谁先完成的,然后就可以处理结果或告诉用户下载失败了。

这里,应该再说明两件事:

  1. 类似的方法在libqxt项目(http://www.libqxt.org)中QxtSignalWaiter类的组成部分。

  2. 对于某些操作,Qt提供了一个“等待”方法(例如:QIODevice::waitForBytesWritten()),作用或多或少与上面的代码相同,但没有运行事件循环。然而,“等待”的解决方案将阻塞GUI,因为它们不运行自己的事件循环。

逐步地解决问题

如果能把问题分成子问题,然后有一种不阻塞GUI的很好的方案。简而言之,你可以在较短步骤内执行任务不会阻塞耗时处理的事件。当发现在指定的任务上已经花了一些时间时,保存其状态并返回到事件循环。在完成事件后,需要有一种方法来通知Qt继续你的任务。

比较幸运,有两种方式。

  • QTimer::singleShot()

  • QMetaObject::invokeMethod()

方式一:使用一个单次触发定时器(时间间隔为0)。这个特殊的值会导致Qt发出timeout()信号,定时器的事件循环将会变为空闲。如果连接这个信号至一个槽函数,当应用程序不忙着做其它事情(类似于屏幕保护的工作机制)时,会得到调用函数的机制。这里我们看一个后台查找素数的例子:

class FindPrimes : public QObject
{
Q_OBJECT
public:
FindPrimes(QObject *parent = 0) : QObject(){}
public slots:
void start(qlonglong _max);
private slots:
void calculate();
signals:
void prime(qlonglong);
void finished();
private:
qlonglong cand, max, curr;
double sqrt;
void next(){ cand+=2; curr = 3; sqrt = ::sqrt(cand);}
}; void FindPrimes::start(qlonglong _max)
{
emit prime(1); emit prime(2); emit prime(3);
max = _max; cand = 3; curr = 3;
next();
QTimer::singleShot(0, this, SLOT(calculate()));
} void FindPrimes::calculate()
{
QTime t;
t.start();
while (t.elapsed() < 150) {
if (cand > max) {
emit finished(); // 结束
return;
}
if (curr > sqrt) {
emit prime(cand); // 素数
next();
} else if (cand % curr == 0)
next(); // 非素数
else
curr += 2; // 查找下一个素数
}
QTimer::singleShot(0, this, SLOT(calculate()));
}

FindPrimes类使用两种特性 - 保持其当前计算状态(cand、curr变量),以便它可以继续计算中断的地方,并且可以监控(通过使用QTime::elapsed())当前任务步骤执行了多长时间。如果时间超过预定量,就返回到事件循环。但这样做之前,它启动一个会再次调用该方法的单次定时器(你可能把这种方式称之为“延迟复发”)。

方式二:这种方法可以调用任何对象中的任何槽函数。需要说的一件事是,这在我们的情况下工作,我们需要确保使用Qt::QueuedConnection连接类型,使槽函数以异步方式(默认情况下,在一个单线程中调用槽函数是同步的)调用。因此,我们可能会以下列取代计时器调用:

QMetaObject::invokeMethod(this, "calculate", Qt::QueuedConnection);

使用这种方式比定时器好的地方在于,它可以传递参数给槽函数(例如:给它传递当前的计算状态),除此以外,这两种方法是等效的。

并行编程

最后,还有这种情况 - 需要为一组数据执行类似的操作。例如:为一个目录的图片创建缩略图,来看一下最简单的实现:

QList<QImage> images = loadImages(directory);
QList<QImage> thumbnails;
foreach (const QImage &image, images) {
thumbnails << image.scaled(QSize(300,300), Qt::KeepAspectRatio, Qt::SmoothTransformation);
QCoreApplication::sendPostedEvents();
}

这种方法的缺点是:创建一个单一的缩略图可能会花很长时间,那时候GUI将会阻塞,更好的方法是在一个单独的线程中执行:

QList<QImage> images = loadImages(directory);
ThumbThread *thread = new ThumbThread;
connect(thread, SIGNAL(finished(QList<QImage>)), this, SLOT(showThumbnails(QList<QImage>)));
thread->start(images);

这个解决方案非常好,但没有考虑到计算机系统发展的方向。越来越快的处理单元配备多个慢单元(多核或多处理器系统),它们一起提供更多的计算周期伴随着低功耗和散热。不幸的是,上述算法只使用了一个线程,因此在单个处理单元上执行,导致在多核系统上执行比单核的慢(因为多核系统中一个核比单核系统中一个核慢)。

为了克服这一缺点,我们必须进入并行编程的世界 - 将工作划分为尽可能多的线程来处理可用的单元,这些由QThreadPool和Qt Concurrent提供。

第一种可能的做法是使用所谓的runnables-simple类,它的实例可以被一个线程执行。Qt通过QRunnable类来实现runnables。你可以基于QRunnable提供的接口实现属于自己的runnable,并且使用Qt提供的另一个实体执行它。指的是线程池 - 一个可以产生大量线程执行任意工作的对象。如果作业数超过可用的线程数,作业将会排队,当线程可用时作业将会执行。

回到示例,实现runnable,使用线程池创建一个图像的缩略图。

class ThumbRunnable : public QRunnable {
public:
ThumbRunnable(...) : QRunnable(), ... {}
void run(){ m_result = m_image.scaled(...); }
const QImage &result() const{ return m_result; }
}; QList<ThumbRunnable *> runnables;
foreach(const QImage &image, images){
ThumbRunnable *r = new ThumbRunnable(image, ...);
r->setAutoDelete(false);
QThreadPool::globalInstance()->start(r);
runnables << r;
}

基本上,需要做的就是通过QRunnable类实现run()函数,它和子类化QThread相同,唯一的区别是:作业是不依赖于它创建的一个线程,因此可以通过任何现有的任何线程调用。创建ThumbRunnable的一个实例后,我们要确保在作业执行完成以后,它不会被线程池删除。这样做是因为我们想获取对象的结果。最后,我们要求线程池对作业排队利用每个应用程序的全局线程池变量,并添加runnable到列表中以供将来参考。

然后,我们需要定期检查每个runnable,看看它的结果是否可用的,比较无聊、麻烦。幸好,当你需要获取结果时,有一个更好的方案。Qt Concurrent引入了一系列示例可以执行SIMD(单指令多数据)操作。下面我们来看看其中的一个,最简单的一种是处理容器中的每个元素,把结果保存到另一个容器中。

typedef QFutureWatcher<QImage> ImageWatcher;
QImage makeThumb(const QString &img)
{
return QImage(img).scaled(QSize(300,300), ...);
} QStringList images = imageEntries(directory);
ImageWatcher *watcher = new ImageWatcher(this);
connect(watcher, SIGNAL(progressValueChanged(int)), progressBar, SLOT(setValue(int)));
QFuture<QImage> result = QtConcurrent::mapped(images, makeThumb);
watcher->setFuture(result);

很简单,不是吗?只需要几行代码,观察者就会通知我们QFuture对象持有的SIMD程序的状态。它甚至会让我们取消、暂停和恢复程序。这里,我们使用了一个调用最简单可能的变量 - 使用一个独立函数。真实的情况下,会用一些更复杂的东西而不仅仅是一个简单的函数。Qt Concurrent允许使用函数、类函数和函数对象,第三方解决方案可让通过使用绑定函数参数进一步扩大可能性。

总结

上面,已经展示了基于Qt耗时操作类型和复杂度问题的整体解决方案。这些只是基础知识,可以依赖它们 - 例如:使用本地事件循环创建自己的“模式”对象,使用并行编程快速处理数据、或者使用线程池处理通用作业运行。对于简单情况而言,有办法手动请求应用程序来处理挂起事件,将复杂的任务划分成更小的子任务可能是正确的方向。

再也不要让你的GUI阻塞了!

更多参考

Qt之保持GUI响应的更多相关文章

  1. 第15.9节 PyQt学习入门:使用Qt Designer进行GUI设计的步骤

    在使用Qt Designer进行GUI设计时,一般常规的步骤都是差不多的,主要步骤包括新建显示窗口.在窗口上按照规划的布局放置组件.设置初始化组件的属性.定义信号和槽函数的连接,一般后三步是每增加一个 ...

  2. 使用 PySide2 开发 Maya 插件系列一:QT Designer 设计GUI, pyside-uic 把 .ui 文件转为 .py 文件

    使用 PySide2 开发 Maya 插件系列一:QT Designer 设计GUI, pyside-uic 把 .ui 文件转为 .py 文件 前期准备: 安装 python:https://www ...

  3. 保持Qt GUI响应的几种方法

    最开始使用Qt时就遇到过QT Gui失去响应的问题,我是用多线程的方式解决的,然而通常来说,多线程是会降低程序的运行速度. 之后,在使用QSqlQuery::execBatch()函数时,Qt Gui ...

  4. Qt多线程和GUI界面假死(run()是线程的入口,就像main()对于应用程序的作用。分析QThread::exec函数的源码,旧的QMutexLocker模式其实很好用,挡住别人进入抢占资源,可照抄)good

    QThread的常见特性: run()是线程的入口,就像main()对于应用程序的作用.QThread中对run()的默认实现调用了exec(),从而创建一个QEventLoop对象,由其处理该线程事 ...

  5. ROS:使用Qt Creator创建GUI程序(一)

    开发环境: Ubuntu14.04 ROS indigo version Qt Creator 3.0.1 based on Qt 5.2.1 步骤如下:(按照下面命令一步步来,亲测可行) (一)安装 ...

  6. QT +go 开发 GUI程序

      ,转载 https://blog.csdn.net/lanbery/article/details/81745611 如果你是一个墨守成规的coding,请移步其他内容,这部分内容可能不适合你.如 ...

  7. Qt和其它GUI库的对比

    http://c.biancheng.net/view/3876.html 世界上的 GUI 库多如牛毛,有的跨平台,有的专属于某个操作系统:有的只有 UI 功能,有的还融合了网络通信.多媒体处理.数 ...

  8. qt开发ROS gui界面环境配置过程总结

    这段时间花了点时间配置了在qtcreator5.9.1上开发ros gui界面的环境,终于可以实现导入工程,插断点调试了.总结起来需要注意以下几点: 1.安装插件ros_qtc_plugin,ROS与 ...

  9. PyQt(Python+Qt)实现的GUI图形界面应用程序的事件捕获方法大全及对比分析

    一. 概述 PyQt的图形界面应用中,事件处理类似于Windows系统的消息处理.一个带图形界面的应用程序启动后,事件处理就是应用的主循环,事件处理负责接收事件.分发事件.接收应用处理事件的返回结果, ...

随机推荐

  1. Page_Load 事件

    Page_Load 事件是众多 ASP.NET 可理解的事件之一.Page_Load 事件会在页面加载时被触发,然后 ASP.NET 会自动调用子例程 Page_Load<%@ Page Lan ...

  2. BZOJ 1927 星际竞速(最小费用最大流)

    题目链接:http://61.187.179.132/JudgeOnline/problem.php?id=1927 题意:一个图,n个点.对于给出的每条边 u,v,w,表示u和v中编号小的那个到编号 ...

  3. sql 增加字段

    ALTER TABLE [dt_article_goods] ADD [goods_id] int DEFAULT 0

  4. CodeForces 131A cAPS lOCK

    cAPS lOCK Time Limit:500MS     Memory Limit:262144KB     64bit IO Format:%I64d & %I64u Submit St ...

  5. 03_Spring工厂接口

    Spring工厂接口 1.BeanFactory 接口 和 ApplicationContext 接口区别 ?      * ApplicationContext 接口继承BeanFactory接口, ...

  6. [Effective Java]第三章 对所有对象都通用的方法

    声明:原创作品,转载时请注明文章来自SAP师太技术博客( 博/客/园www.cnblogs.com):www.cnblogs.com/jiangzhengjun,并以超链接形式标明文章原始出处,否则将 ...

  7. hiho #1050 : 树中的最长路 树的直径

    #1050 : 树中的最长路 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 上回说到,小Ho得到了一棵二叉树玩具,这个玩具是由小球和木棍连接起来的,而在拆拼它的过程中, ...

  8. CSS笔记(五)字体

    CSS 字体属性定义文本的字体系列.大小.加粗.风格(如斜体)和变形(如小型大写字母). 参考:http://www.w3school.com.cn/css/css_font.asp CSS字体系列 ...

  9. 将客户端将IE9强制为IE7

    有时候由于浏览器的问题我们在IE7中开发的东西需要在IE9中展示 但是会出现兼容性的问题. 那么我们可以同技巧将用户端的浏览器强行以IE7的文档模式展示我们的网页 下面是针对iis asp.net程序 ...

  10. HIHO线段树(成段)

    #include <stdio.h> #define lson l,mid,id<<1 #define rson mid+1,r,id<<1|1 ; ],lazy[ ...