GUI线程和工作线程
每个程序在启动时都有一个线程。这个线程被称为“主线程”(在Qt应用程序中也称为“GUI线程”)。Qt GUI必须在这个线程中运行。所有部件和一些相关的类,例如QPixmap,都不能在次要线程中工作。次要线程通常被称为“工作线程”,因为它用于从主线程卸载处理工作。
同步访问数据
每个线程都有自己的栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块是如何位于内存中的。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个独立的栈。
编辑
如果两个线程都有一个指向同一个对象的指针,则两个线程可能同时访问该对象,这可能会潜在地破坏对象的完整性。很容易想象,当同时执行同一个对象的两个方法时,可能会出现很多问题。
有时需要从不同的线程访问同一个对象;例如,当处于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程交换数据比进程更容易、更快。数据不一定要序列化和复制。传递指针是可能的,但必须严格协调哪个线程接触哪个对象。必须防止在一个对象上同时执行操作。有几种方法可以实现这一点,下面将介绍其中一些方法。
那么什么是安全的呢?在一个线程中创建的所有对象都可以在该线程中安全地使用,只要其他线程没有对它们的引用,并且对象没有与其他线程进行隐式耦合。这种隐式耦合可能发生在实例之间共享数据时,例如与静态成员、单例或全局数据共享数据。熟悉线程安全和可重入类和函数的概念。
使用线程
基本上有两种线程用例:
- 利用多核处理器加快处理速度。
- 通过卸载长时间处理或阻塞对其他线程的调用来保持GUI线程或其他时间关键线程的响应。
线程的替代方案
开发人员需要非常小心地使用线程。启动其他线程很容易,但要确保所有共享数据保持一致却非常困难。问题通常很难发现,因为它们可能只在一段时间内出现,或者只在特定的硬件配置上出现。在创建线程来解决某些问题之前,应该考虑可能的替代方案。
| Alternative | Comment |
|---|---|
| QEventLoop::processEvents() | 在耗时的计算过程中反复调用QEventLoop::processEvents()可以防止GUI阻塞。然而,这种解决方案不能很好地扩展,因为对processEvents()的调用可能发生得太频繁,或者不够频繁,这取决于硬件。(这种方案会造成信号槽错乱,不推荐) |
| QTimer | 后台处理有时可以方便地使用计时器在将来的某个时间点调度插槽的执行。当没有更多的事件需要处理时,间隔为0的计时器就会超时。(推荐) |
| QSocketNotifier QNetworkAccessManager QIODevice::readyRead() | 这是使用一个或多个线程的替代方案,每个线程在缓慢的网络连接上读取时会阻塞。只要响应网络数据块的计算能够快速执行,这种响应式设计就比线程中的同步等待要好。响应式设计比线程式设计更不容易出错,而且更节能。在很多情况下,它还能带来性能上的好处。 |
应该使用哪种Qt线程技术?
Qt提供了许多处理线程的类和函数。下面是Qt程序员可以用来实现多线程应用程序的四种不同方法。
QThread:带有可选事件循环的低级API
QThread是Qt中所有线程控制的基础,每个QThread实例代表并控制一个线程。
QThread既可以被直接实例化,也可以被子类化。实例化QThread提供了一个并行事件循环,允许在次要线程中调用QObject槽。子类化QThread允许应用程序在开始事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。
有关如何使用QThread 的演示,请参见QThread class reference 和线程示例。
QThreadPool和QRunnable:重用线程
频繁地创建和销毁线程的代价是昂贵的。为了减少这种开销,现有线程可以被新任务重用。QThreadPool是可重用的qthread的集合。
要在QThreadPool的某个线程中运行代码,请重新实现QRunnable::run()并实例化子类QRunnable。使用QThreadPool::start() 将QRunnable 置于QThreadPool 的运行队列中。当线程可用时,QRunnable::run() 中的代码将在该线程中执行。
每个 Qt XML 应用程序都有一个全局线程池,可通过QThreadPool::globalInstance() 访问。全局线程池会根据 CPU 的内核数自动维护最佳线程数。不过,也可以明确创建和管理单独的QThreadPool 。
class HelloWorldTask : public QRunnable
{
void run() override
{
qDebug() << "Hello world from thread" << QThread::currentThread();
}
};
HelloWorldTask *hello = new HelloWorldTask();
// QThreadPool takes ownership and deletes 'hello' automatically
QThreadPool::globalInstance()->start(hello);
Qt Concurrent:使用高级API
该 Qt Concurrent模块提供了处理一些常见并行计算模式的高级函数:map、filter 和 reduce。与使用QThread 和QRunnable 不同,这些函数从不要求使用互斥或信号等低级线程原语。相反,这些函数会返回一个QFuture 对象,当函数准备就绪时,可使用该对象检索函数结果。QFuture 也可用于查询计算进度,以及暂停/继续/取消计算。为方便起见,QFutureWatcher 可通过信号和插槽与QFuture进行交互。
该 Qt Concurrent模块提供了处理一些常见并行计算模式的高级函数:map、filter 和 reduce。与使用QThread 和QRunnable 不同,这些函数从不要求使用互斥或信号等低级线程原语。相反,这些函数会返回一个QFuture 对象,当函数准备就绪时,可使用该对象检索函数结果。QFuture 也可用于查询计算进度,以及暂停/继续/取消计算。为方便起见,QFutureWatcher 可通过信号和插槽与QFuture进行交互。
Qt Concurrent该模块的映射、过滤和还原算法会自动将计算分配给所有可用的处理器内核,因此现在编写的应用程序以后部署到内核更多的系统上时仍可继续扩展。
该模块还提供了QtConcurrent::run() 函数,可以在另一个线程中运行任何函数。不过,QtConcurrent::run() 只支持 map、filter 和 reduce 函数可用的部分功能。QFuture 可用于获取函数的返回值,并检查线程是否正在运行。不过,调用QtConcurrent::run() 只能使用一个线程,不能暂停/恢复/取消,也不能查询进度。
请参阅 Qt Concurrent模块文档,了解各个函数的详细信息。
Qt Concurrent
Qt Concurrent 模块提供了高级应用程序接口,使编写多线程程序成为可能,而无需使用互斥、读写锁、等待条件或 Semaphores 等低级线程原语。使用Qt Concurrent 编写的程序会根据可用处理器内核的数量自动调整使用的线程数。这意味着现在编写的应用程序将来在多核系统上部署时仍能继续扩展。
Qt Concurrent 并行 MapReduce 包括用于并行列表处理的函数式编程 API,包括用于共享内存(非分布式)系统的 MapReduce 和 FilterReduce 实现,以及用于管理 GUI 应用程序中异步计算的类:
-
- QtConcurrent::map()会对容器中的每个项目应用一个函数,对项目进行就地修改。
- QtConcurrent::mapped() 类似于 map(),但它返回的是一个包含修改内容的新容器。
- QtConcurrent::mappedReduced() 类似于 mapped(),只不过修改后的结果被缩小或折叠成一个结果。
-
- QtConcurrent::filter() 会根据过滤函数的结果删除容器中的所有项目。
- QtConcurrent::filtered() 与 filter() 类似,但它返回的是一个包含过滤结果的新容器。
- QtConcurrent::filteredReduced() 与 filtered() 类似,只是过滤后的结果被缩小或折叠为一个结果。
-
- QtConcurrent::run() 在另一个线程中运行一个函数。
-
- QtConcurrent::task() 将创建QtConcurrent::QTaskBuilder 的实例。该对象可用于调整参数和在单独的线程中启动任务。
-
QFuture 表示异步计算的结果。
-
QFutureIterator 可以遍历通过 获得的结果。QFuture
-
QFutureWatcher 使用信号和插槽可以监控 。QFuture
-
QFutureSynchronizer 是一个方便的类,可自动同步多个 QFutures。
-
QPromise 提供了一种向 报告异步计算的进度和结果的方法。当 请求时,允许暂停或取消任务。QFuture QFuture
Qt Concurrent map 和 filter 函数支持几种 STL 兼容的容器和迭代器类型,但与具有随机访问迭代器的 Qt 容器(如 )配合使用效果最佳。map 和 filter 函数同时接受容器和开始/结束迭代器。QList
STL 迭代器支持概述:
| 迭代器类型 | 示例类 | 支持状态 |
|---|---|---|
| 输入迭代器 | 不支持 | |
| 输出迭代器 | 不支持 | |
| 前向迭代器 | std::slist | 支持 |
| 双向迭代器 | std::list | 支持 |
| 随机存取迭代器 | QList,std::矢量 | 支持和推荐 |
在Qt Concurrent 对大量轻量级项目进行迭代时,随机存取迭代器的速度会更快,因为它们允许跳转到容器中的任意点。此外,使用随机存取迭代器还可以让Qt Concurrent 通过QFuture::progressValue() 和QFutureWatcher::progressValueChanged() 提供进度信息。
非就地修改函数(如 mapped() 和 filtered())会在调用时复制容器。如果使用的是 STL 容器,复制操作可能需要一些时间,在这种情况下,我们建议指定容器的开始和结束迭代器。
WorkerScript: QML中的线程
WorkerScript QML类型允许JavaScript代码与GUI线程并行运行。
每个WorkerScript实例可以附加一个.js脚本。当WorkerScript.sendMessage()被调用时,脚本将在一个单独的线程(和一个单独的QML上下文)中运行。当脚本完成运行时,它可以将应答发送回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 | ||||
| 线程可以通过信号接收数据更新 | Yes (received by a worker QObject) | Yes (received by WorkerScript) | |||
| 线程可以使用信号来控制 | Yes (received by QThread) | Yes (received by QFutureWatcher) | |||
| 线程可以通过QFuture来监控 | Partially | Yes | |||
| 内置暂停/恢复/取消功能 | Yes |
用例
| 线程寿命 | Operation | Solution |
|---|---|---|
| 一次 | 在另一个线程中运行一个新的线性函数,可以选择在运行期间进行进度更新。 | Qt提供了不同的解决方案:- 将函数放在QThread::run()的重新实现中,并启动QThread。发送信号来更新进度。或 |
- 将函数放在QRunnable::run()的重新实现中,并将QRunnable添加到QThreadPool中。写入线程安全的变量以更新进度。或
- 使用QtConcurrent:: Run()运行函数。写入线程安全的变量以更新进度。 | | 一次 | 在另一个线程中运行现有函数并获取其返回值。 | 使用QtConcurrent:: Run()运行函数。让QFutureWatcher在函数返回时发出finished()信号,并调用QFutureWatcher::result()来获取函数的返回值。 | | 一次 | 对容器的所有项执行操作,使用所有可用的内核。例如,从图像列表生成缩略图。 | 使用QtConcurrent的QtConcurrent::filter()函数选择容器元素,使用QtConcurrent::map()函数对每个元素应用操作。要将输出折叠为单个结果,请使用QtConcurrent::filteredReduced()和QtConcurrent::mappedReduced()。 | | 一次/永久 | 在纯QML应用程序中执行长时间的计算,并在结果准备好时更新GUI。 | 将计算代码放在。js脚本中,并将其附加到WorkerScript实例。调用WorkerScript.sendMessage()在新线程中开始计算。让脚本也调用sendMessage(),将结果传递回GUI线程。在onMessage中处理结果并更新GUI。 | | 永久 | 让一个对象驻留在另一个线程中,该线程可以根据请求执行不同的任务和/或可以接收新数据来处理。 | 子类化一个QObject来创建一个worker。实例化这个worker对象和一个QThread。将工作线程移动到新线程。通过排队信号槽连接向工作对象发送命令或数据。 | | 永久 | 在另一个线程中重复执行昂贵的操作,其中线程不需要接收任何信号或事件。 | 直接在QThread::run()的重新实现中编写无限循环。启动不使用事件循环的线程。让线程发出信号,将数据发送回GUI线程。 |
线程同步
参考:Synchronizing Threads | Qt 5.15
线程安全
参考:Reentrancy and Thread-Safety | Qt 5.15
线程与QObjects
Threads and QObjects | Qt 5.15
Thread Support in Qt | Qt 5.15
Multithreading Technologies in Qt | Qt 5.15