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