目录
每个程序在启动时都有一个线程。这个线程被称为“主线程”(在Qt应用程序中也称为“GUI线程”)。Qt GUI必须在这个线程中运行。所有部件和一些相关的类,例如QPixmap,都不能在次要线程中工作。次要线程通常被称为“工作线程”,因为它用于从主线程卸载处理工作。
每个线程都有自己的栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块是如何位于内存中的。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个独立的栈。

如果两个线程都有一个指向同一个对象的指针,则两个线程可能同时访问该对象,这可能会潜在地破坏对象的完整性。很容易想象,当同时执行同一个对象的两个方法时,可能会出现很多问题。
有时需要从不同的线程访问同一个对象;例如,当处于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程交换数据比进程更容易、更快。数据不一定要序列化和复制。传递指针是可能的,但必须严格协调哪个线程接触哪个对象。必须防止在一个对象上同时执行操作。有几种方法可以实现这一点,下面将介绍其中一些方法。
那么什么是安全的呢?在一个线程中创建的所有对象都可以在该线程中安全地使用,只要其他线程没有对它们的引用,并且对象没有与其他线程进行隐式耦合。这种隐式耦合可能发生在实例之间共享数据时,例如与静态成员、单例或全局数据共享数据。熟悉线程安全和可重入类和函数的概念。
连接只能在创建它的线程内使用。不支持在线程之间移动连接或从不同的线程创建查询。
此外,QSqlDrivers使用的第三方库可以对在多线程程序中使用SQL模块施加进一步的限制。有关更多信息,请查阅数据库客户端的手册
QPainter可以在线程中使用油漆到QImage, QPrinter和QPicture油漆设备。不支持在QPixmaps和QWidgets上绘制。在macOS上,如果您从GUI线程之外打印,则不会显示自动进度对话框。
任意数量的线程都可以在任何给定的时间进行绘制,但是同一时间只有一个线程可以在给定的绘制设备上进行绘制。换句话说,如果两个线程分别绘制不同的QImage,则可以同时绘制,但两个线程不能同时绘制相同的QImage。
QTextDocument、QTextCursor和所有相关的类都是可重入的。
注意,在GUI线程中创建的QTextDocument实例可能包含QPixmap图像资源。使用QTextDocument::clone()创建文档的副本,并将副本传递给另一个线程进行进一步处理(例如打印)。
QtSvg模块中的QSvgGenerator和QSvgRenderer类是可重入的。
Qt使用了一种称为隐式共享(implicit sharing)的优化方法来处理它的很多值,尤其是QImage和QString。从Qt 4开始,隐式共享类可以安全地跨线程复制,就像任何其他值类一样。它们是完全可重入的。隐式分享真的是隐式的。
在许多人的心目中,隐式共享和多线程是不兼容的概念,因为引用计数通常是这样完成的。然而,Qt使用原子引用计数来确保共享数据的完整性,避免了引用计数器的潜在损坏。
注意,原子引用计数不能保证线程安全。当线程之间共享隐式共享类的实例时,应该使用适当的锁。这对所有可重入类(无论是否共享)都是相同的要求。然而,原子引用计数确实可以保证一个独立工作的线程,隐式共享类的本地实例是安全的。我们建议使用信号和槽在线程之间传递数据,因为这可以在不需要任何显式锁的情况下完成。
总结一下,Qt 4中的隐式共享类确实是隐式共享的。即使在多线程应用程序中,你也可以安全地使用它们,就像它们是普通的、非共享的、可重入的基于值的类一样
基本上有两种线程用例:
开发人员需要非常小心地使用线程。启动其他线程很容易,但要确保所有共享数据保持一致却非常困难。问题通常很难发现,因为它们可能只在一段时间内出现,或者只在特定的硬件配置上出现。在创建线程来解决某些问题之前,应该考虑可能的替代方案。
| Alternative | Comment |
|---|---|
| QEventLoop::processEvents() | 在耗时的计算过程中反复调用QEventLoop::processEvents()可以防止GUI阻塞。然而,这种解决方案不能很好地扩展,因为对processEvents()的调用可能发生得太频繁,或者不够频繁,这取决于硬件。(这种方案会造成信号槽错乱,不推荐) |
| QTimer | 后台处理有时可以方便地使用计时器在将来的某个时间点调度插槽的执行。当没有更多的事件需要处理时,间隔为0的计时器就会超时。(推荐) |
| QSocketNotifier QNetworkAccessManager QIODevice::readyRead() | 这是使用一个或多个线程的替代方案,每个线程在缓慢的网络连接上读取时会阻塞。只要响应网络数据块的计算能够快速执行,这种响应式设计就比线程中的同步等待要好。响应式设计比线程式设计更不容易出错,而且更节能。在很多情况下,它还能带来性能上的好处。 |
Qt提供了许多处理线程的类和函数。下面是Qt程序员可以用来实现多线程应用程序的四种不同方法。
QThread是Qt中所有线程控制的基础,每个QThread实例代表并控制一个线程。
QThread既可以被直接实例化,也可以被子类化。实例化QThread提供了一个并行事件循环,允许在次要线程中调用QObject槽。子类化QThread允许应用程序在开始事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。
See the QThread class reference and the threading examples for demonstrations on how to use QThread.
频繁地创建和销毁线程的代价是昂贵的。为了减少这种开销,现有线程可以被新任务重用。QThreadPool是可重用的qthread的集合。
要在QThreadPool的某个线程中运行代码,请重新实现QRunnable::run()并实例化子类QRunnable。使用QThreadPool::start()将QRunnable放入QThreadPool的运行队列中。当线程可用时,QRunnable::run()中的代码将在该线程中执行。
每个Qt应用程序都有一个全局线程池,可以通过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模块提供了一些高级函数来处理一些常见的并行计算模式:map、filter和reduce。与使用QThread和QRunnable不同,这些函数从来不需要使用诸如互斥量或信号量之类的低级线程原语。相反,它们返回一个QFuture对象,当函数准备好时,该对象可用于检索函数的结果。QFuture还可以用于查询计算进度,暂停/恢复/取消计算。为了方便,QFutureWatcher允许通过信号和插槽与qfuture进行交互。
Qt Concurrent的map、filter和reduce算法会根据可用的处理器核数自动调整所使用的线程数,所以今天编写的应用程序在以后部署到更多内核的系统上时仍然可以扩展。
这个模块还提供了QtConcurrent::run()函数,它可以在另一个线程中运行任何函数。然而,QtConcurrent::run()只支持map、filter和reduce函数可用的功能子集。QFuture可用于获取函数的返回值,并检查线程是否正在运行。但是,对QtConcurrent::run()的调用只使用一个线程,不能被暂停/恢复/取消,也不能查询进度。
有关各个函数的详细信息,请参阅Qt Concurrent模块文档。
QtConcurrent::run:(在另一个线程中运行一个函数。)
- // call 'void QImage::invertPixels(InvertMode mode)' in a separate thread
- QImage image = ...;
- QFuture<void> future = QtConcurrent::run(&image, &QImage::invertPixels, QImage::InvertRgba);
- ...
- future.waitForFinished();
- // At this point, the pixels in 'image' have been inverted
Qt Concurrent 支持多种兼容 STL 的容器和迭代器类型,但是最好使用具有随机访问迭代器的 Qt 容器,例如:QList 或 QVector。map 和 filter 函数都接受容器和 begin/end 迭代器。
Concurrent Map 和 Map-Reduce:多线程处理容器里的项
QtConcurrent::map():将一个函数应用于一个容器中的每一项,直接修改 items。
QtConcurrent::mapped():和 map() 类似,只是它返回一个包含修改内容的新容器。
QtConcurrent::mappedReduced():和 mapped() 类似,只是修改后的结果减少或组合成一个单一的结果。
- 函数模版格式:
- U function(T &t);//T必须是容器里的类型,U必须是void(没有返回值和返回类型)
-
- 例子:
- void scale(QImage &image)
- {
- image = image.scaled(100, 100);
- }
-
- QList
images = ...; - QFuture<void> future = QtConcurrent::map(images, scale);
- 函数模版格式:
- U function(const T &t);//T必须是容器里的类型,U可以任意类型(最终结果)
-
- 例子:
- QImage scaled(const QImage &image)
- {
- return image.scaled(100, 100);
- }
-
- QList
images = ...;//QList里的每张图片都会被缩放 - QFuture
thumbnails = QtConcurrent::mapped(images, scaled); - //多个线程同时运行,每个结果通过QFuture返回。
- 函数模版:
- V必须是void(没有返回值和返回类型),T是mappedReduced后返回的结果(最终结果),U是scaled函数的返回类型。
- V function(T &result, const U &intermediate)
-
- QImage scaled(const QImage &image)
- {
- return image.scaled(100, 100);
- }
-
- void addToCollage(QImage &collage, const QImage &thumbnail)
- {
- QPainter p(&collage);
- static QPoint offset = QPoint(0, 0);
- p.drawImage(offset, thumbnail);
- offset += ...;
- }
-
- QList
images = ...; - //多个线程同时运算,addToCollage每次只有一个线程调用,所有是线程安全的
- QFuture
collage = QtConcurrent::mappedReduced(images, scaled, addToCollage); - //理解:images对象调用scaled,scaled的结果通过addToCollage的第二个参数传递给addToCollage,addToCollage运算最终结果通过第一个参数返回。
同步模式,直接返回结果:
- QList
images = ...; -
- // Each call blocks until the entire operation is finished.
- QList
future = QtConcurrent::blockingMapped(images, scaled); -
- QtConcurrent::blockingMap(images, scale);
-
- QImage collage = QtConcurrent::blockingMappedReduced(images, scaled, addToCollage);
Concurrent Filter 和 Filter-Reduce:对容器里的项过滤后再多线程处理
QtConcurrent::filter():对结果进行筛选(根据筛选函数)。
QtConcurrent::filtered():和 filter() 类似,只是它返回一个包含过滤内容的新容器。
QtConcurrent::filteredReduced():和 filtered() 类似,只是过滤后的结果减少或组合成一个单一的结果。
- //T必须匹配存储在序列中的类型。如果应该保留该项,则返回true;如果应该丢弃该项,则返回false。
- bool function(const T &t);
-
- bool allLowerCase(const QString &string)
- {
- return string.lowered() == string;
- }
-
- QStringList strings = ...;
- //对每一个项的结果都会执行过滤函数判断是否丢弃掉。
- QFuture
lowerCaseStrings = QtConcurrent::filtered(strings, allLowerCase); -
-
- QStringList strings = ...;
- QFuture<void> future = QtConcurrent::filter(strings, allLowerCase);
- V是void(没有返回值和返回类型),T是最终结果,U是被过滤项目的类型。
- V function(T &result, const U &intermediate)
-
- void addToDictionary(QSet
&dictionary, const QString &string) - {
- dictionary.insert(string);
- }
-
- QStringList strings = ...;
- //每个结果都经过addToDictionary筛选,通过第一个参数返回最终结果
- //QtConcurrent::filteredReduced()保证一次只有一个线程调用reduce,所以没有必要使用互斥锁来锁定结果变量
- QFuture
> dictionary = QtConcurrent::filteredReduced(strings, allLowerCase, addToDictionary);
QFutureIterator:允许通过 QFuture 遍历可用的结果
QFutureWatcher:允许使用信号槽来监控一个 QFuture
QFutureSynchronizer:是一个方便的类,用于一些 QFutures 的自动同步
- // Instantiate the objects and connect to the finished signal.
- MyClass myObject;
- QFutureWatcher<int> watcher;
- connect(&watcher, &QFutureWatcher<int>::finished, &myObject, &MyClass::handleFinished);
-
- // Start the computation.
- QFuture<int> future = QtConcurrent::run(...);
- watcher.setFuture(future);
- void someFunction()
- {
- QFutureSynchronizer<void> synchronizer;
-
- ...
-
- synchronizer.addFuture(QtConcurrent::run(anotherFunction));
- synchronizer.addFuture(QtConcurrent::map(list, mapFunction));
-
- return; // QFutureSynchronizer waits for all futures to finish
- }
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提供了不同的解决方案:
|
| 一次 | 在另一个线程中运行现有函数并获取其返回值。 | 使用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
Threads and QObjects | Qt 5.15