Multithreading Technologies in Qt

Qt 提供一系列的类与函数来处理多线程. Qt 开发者们可以使用下面四种方法来实现多线程应用.

QThread: 低级 API 与可选的事件循环

作为 Qt 线程控制的基础, 每一个 QThread 实例都代表并控制着一个线程.

你可以直接实例化QThread, 或建立子类. 实例化一个 QThread 将附带一个并行事件循环, 允许 QObject 槽函数在子线程执行. 若子类化一个 QThread, 程序可以在事件循环启动前初始化这个新线程, 或者在无事件循环下运行并行代码.

如何使用QThread, 参见 QThread class referencethreading examples.

QThreadPool 和 QRunnable: 线程重用

频繁地创建与销毁线程会消耗很大的系统资源. 为了减少这样额外的开销, 可以重复使用一些现成的线程来执行新的任务. QThreadPool 就是这样一个保存着可重用的 QThread 的集合.

为了将代码放入 QThreadPool的线程中运行, 可以重写 QRunnable::run() 函数并实例化继承自 QRunnable 的子类. 调用 QThreadPool::start() 函数可将 QRunnable 添加到 QThreadPool的运行队列. 一旦出现了一个可用的线程, 它将会执行 QRunnable::run() 里的代码.

每一个 Qt 程序都会自带一个公共线程池, 可以通过调用 QThreadPool::globalInstance() 获取. 线程数为基于 CPU 核心数计算的最佳值. 不过, 你也可以显式创建并管理一个独立的 QThreadPool.

Qt Concurrent: 使用高级 API

The Qt Concurrent 块提供了数个高级函数, 用于处理一些常见的并行计算模式: map, filter, 和 reduce. 不同于使用 QThreadQRunnable, 这些高级函数不需要使用 底层线程原语, 比如互斥锁与信号量. 取而代之的是返回一个 QFuture 对象, 它能够在传入的函数返回值就绪后检索该结果. QFuture 既可以用来查询计算进度, 也可以暂停/恢复/取消计算. 方便起见, QFutureWatcher 可以让您通过信号槽与 QFuture 进行交互.

Qt Concurrent的 map, filter 和 reduce 算法会自动将计算过程分配到可用的处理器核心, 由此, 当下编写的程序在以后部署到更多核心的系统上时会被自动扩展.

此模块还提供了 QtConcurrent::run() 函数, 可以将任何函数在另一个线程中运行. 不过, QtConcurrent::run() 仅提供 map, filter 和 reduce 函数的一部分功能. QFuture 可以用于检索函数返回值, 也可以用于查看线程是否处于运行中. 然而, 调用 QtConcurrent::run() 时只会使用一个线程, 并且无法暂停/恢复/取消, 也不能查询计算进度.

详见 Qt Concurrent.

WorkerScript: QML 中的多线程

The WorkerScript QML 类型 WorkerScript 可将 JavaScript 代码与 GUI 线程并行运行.

每个 WorkerScript 实例可附加一个 .js 脚本. 当调用 WorkerScript::sendMessage() 时, 脚本将会运行在一个独立的线程中 (伴随一个独立的 QML 上下文). 在脚本运行结束后, WorkerScript 将会向 GUI 线程发送回复, 后者会调用 WorkerScript::onMessage() 信号处理函数.

使用 WorkerScript, 很像使用一个移入子线程工作的 QObject, 数据通过信号槽在线程间进行传输.

详见 WorkerScript.

选择合适的方法

如上文所述, Qt 提供了开发多线程应用的不同解决方案. 对一个给定的程序, 需要根据新线程的用途与线程的生命周期来决定正确的方案. 下面是一组 Qt 多线程技术的功能对比表, 以及对于一些范例较为推荐的解决方案.

解决方案对比

FeatureQThreadQRunnable and QThreadPoolQtConcurrent::run()Qt Concurrent (Map, Filter, Reduce)WorkerScript
LanguageC++C++C++C++QML
Thread priority can be specifiedYesYes
Thread can run an event loopYes
Thread can receive data updates through signalsYes (received by a worker QObject)Yes (received by WorkerScript)
Thread can be controlled using signalsYes (received by QThread)Yes (received by QFutureWatcher)
Thread can be monitored through a QFuturePartiallyYes
Built-in ability to pause/resume/cancelYes

Example Use Cases

Lifetime of threadOperationSolution
一次调用在另一个线程中运行新的线性函数, 并选择在运行期间更新进度.Qt 提供不同的解决方案:
一次调用在另一个线程中运行一个已存在的函数, 并获取返回值. 调用 QtConcurrent::run() 运行函数. QFutureWatcher 在函数结束时发出 finished() 信号, 调用 QFutureWatcher::result() 获取函数运行结果.
一次调用对容器中的元素执行统一操作, 如, 对一组图形生成缩略图.使用 Qt Concurrent 的 QtConcurrent::filter() 函数选择容器元素, 使用 QtConcurrent::map() 函数对每个元素执行统一操作. 使用 QtConcurrent::filteredReduced() 和 QtConcurrent::mappedReduced() 输出一个简单的处理结果.
一次调用/常驻在纯 QML 应用程序中执行长时间计算, 并在结果准备好时更新 GUI.将计算代码放入 .js 脚本中并将其附加到 WorkerScript 实例. 调用 WorkerScript::sendMessage() 在新线程中开始计算; 脚本也调用 WorkerScript::sendMessage() 将结果传递回 GUI 线程. 在 onMessage 中处理结果并更新 GUI.
常驻让一个对象存在于另一个线程中, 该对象可以根据请求执行不同的任务和/或可以接收要使用的新数据. 子类化 QObject, 创建一个工作者. 实例化这个工作对象和一个 QThread 对象. 将工作对象移入新线程. 通过信号和槽函数的连接队列发送命令或数据.
常驻在另一个线程中重复执行耗时操作, 该线程不需要接收任何信号或事件. 重新实现 QThread::run() 实现这个操作. 启动没有事件循环的线程, 线程使用信号将数据发给 GUI 线程.