Qt中的多线程技术
前言
Qt提供了许多处理线程的类和函数。下面是Qt程序员可以用来实现多线程应用程序的四种不同方法。
QThread:具有可选事件循环的低级API
QThread类是Qt中所有线程控制的基础,每个QThread实例代表并控制一个线程。
QThread可以被直接实例化,也可以被子类化。实例化QThread提供了一个并行事件循环,允许在辅助线程中调用QObject插槽。子类化QThread允许应用程序在启动其事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。
有关如何使用QThread的演示,请参阅QThread类引用和线程示例(threading examples)。
QThreadPool和QRunnable:重用线程
频繁地创建和销毁线程可能代价高昂。为了减少这种开销,可以为新任务重用现有的线程。QThreadPool是可重用的QThread的集合。
要在一个QThreadPool的线程中运行代码,重新实现QRunnable::run()并实例化子类的QRunnable。使用QThreadPool::start()将QRunnable放到QThreadPool的运行队列中。当线程可用时,QRunnable::run()中的代码将在该线程中执行。
每个Qt应用程序都有一个全局线程池,可以通过QThreadPool::globalInstance()访问。这个全局线程池会根据CPU中的内核数量自动维护一个最佳线程数量。但是,可以显式地创建和管理单独的QThreadPool。
Qt并发:使用高级API
Qt并发模块提供高级函数,用于处理一些常见的并行计算模式:map、filter和reduce。与使用QThread和QRunnable不同,这些函数从不需要使用低级线程元语法,比如互斥或信号量。相反,它们返回一个QFuture对象,当函数准备好时,它可以用来检索函数的结果。QFuture还可以用来查询计算进度和暂停/恢复/取消计算。为了方便,QFutureWatcher可以通过信号和插槽与QFutures进行交互。
Qt Concurrent的map、filter和reduce算法会自动将计算分布到所有可用的处理器内核上,所以今天编写的应用程序在以后部署到拥有更多内核的系统上时将会继续扩展。
这个模块还提供了QtConcurrent::run()函数,它可以在另一个线程中运行任何函数。但是,QtConcurrent::run()只支持map、filter和reduce函数可用的特性子集。QFuture可用于检索函数的返回值,并检查线程是否正在运行。但是,QtConcurrent::run()调用只使用一个线程,不能暂停/恢复/取消,也不能查询进程。
有关各个函数的详细信息,请参阅Qt并发模块文档( Qt Concurrent)。
WorkerScript: QML中的线程
WorkerScript QML类型允许JavaScript代码与GUI线程并行运行。
每个WorkerScript实例可以附加一个.js脚本。当WorkerScript.sendMessage()被调用时,该脚本将在单独的线程(和单独的QML上下文)中运行。当脚本完成运行时,它可以向GUI线程发送回复,GUI线程将调用WorkerScript.onMessage()信号处理程序。
使用WorkerScript类似于使用移动到另一个线程的worker QObject。数据通过信号在线程之间传输。
有关如何实现脚本的详细信息,以及可以在线程之间传递的数据类型列表,请参阅WorkerScript文档。
选择合适的方法
如上所示,Qt为开发线程化应用程序提供了不同的解决方案。对于给定的应用程序,正确的解决方案取决于新线程的用途和线程的生存期。下面是Qt线程技术的比较,然后是一些示例用例的推荐解决方案。
解决方案的比较
Feature | QThread | QRunnable and QThreadPool | QtConcurrent::run() | Qt Concurrent (Map, Filter, Reduce) | WorkerScript |
---|---|---|---|---|---|
语言 | C++ | C++ | C++ | C++ | QML |
可以指定线程优先级 | Yes | Yes | |||
线程可以运行事件循环 | Yes | ||||
线程可以通过信号接收数据更新 | 是(由工作对象QObject接收) | 是(由WorkerScript接收) | |||
线程可以通过信号进行控制 | 是(QThread收到) | 是(由QFutureWatcher接受) | |||
线程可以通过QFuture监视 | Partially | Yes | |||
内置能力暂停/恢复/取消 | Yes |
示例用例
Lifetime of thread | Operation | Solution |
---|---|---|
One call | 在另一个线程中运行一个新的线性函数,可以选择在运行期间进行进度更新。 | Qt提供了不同的解决方案: 将函数放在QThread::run()的重新实现中,然后启动QThread。发出更新进度的信号。或 将函数放在QRunnable::run()的重新实现中,并将QRunnable添加到QThreadPool中。写入线程安全变量以更新进程。或 使用QtConcurrent:: Run()运行函数。写入线程安全变量以更新进程。 |
One call | 在另一个线程中运行现有的函数并获得它的返回值。 | 使用QtConcurrent:: Run()运行函数。让QFutureWatcher在函数返回时发出finish()信号,并调用QFutureWatcher::result()来获取函数的返回值。 |
One call | 使用所有可用的核心对容器的所有项执行操作。例如,从图像列表生成缩略图。 | 使用QtConcurrent的QtConcurrent::filter()函数选择容器元素,使用QtConcurrent::map()函数对每个元素应用操作。要将输出折叠成单个结果,可以使用QtConcurrent:: filteredreduce()和QtConcurrent::mappedReduced()。 |
One call/Permanent | 在纯QML应用程序中进行长时间的计算,并在结果就绪时更新GUI。 | 将计算代码放在.js脚本中,并将其附加到WorkerScript实例中。调用WorkerScript.sendMessage()在一个新线程中开始计算。让脚本也调用sendMessage(),将结果传递回GUI线程。在onMessage中处理结果并更新那里的GUI。 |
Permanent | 让一个对象生活在另一个线程中,该线程可以根据请求执行不同的任务,并且/或者可以接收新的数据来处理。 | 子类化一个QObject来创建一个worker。实例化这个worker对象和一个QThread。将worker移动到新线程。通过排队的信号槽连接向worker对象发送命令或数据。 |
Permanent | 在另一个线程中重复执行开销大的操作,该线程不需要接收任何信号或事件。 | 直接在QThread::run()的重新实现中编写无限循环。在不使用事件循环的情况下启动线程。让线程发出信号将数据发送回GUI线程。 |