【翻译】Qt线程概述

一、关于线程

线程的用途:单个进程内的并发。

1.1、单核CPU

在单核 CPU 上并行工作是一种错觉。对于进程,这种错觉是通过在很短的时间后中断处理器对一个进程的工作而产生的。然后处理器继续下一个过程。为了在进程之间切换,保存当前程序计数器并加载下一个处理器的程序计数器;对寄存器和某些体系结构和操作系统特定数据进行处理。

正如一个 CPU 可以驱动两个或多个进程一样,也可以让 CPU 在一个进程的两个不同代码段上运行。当一个进程启动时,它总是执行一个代码段,该进程被称为主线程。程序可能会决定启动第二个线程。然后,在一个进程中同时处理两个不同的代码序列。在单核 CPU 上通过重复保存程序计数器和寄存器然后加载下一个线程的程序计数器和寄存器来实现并发。在活动线程之间循环不需要程序的合作。当切换到下一个线程时,一个线程可能处于任何状态。

1.2、多核CPU

CPU 设计的当前趋势是具有多个内核。具有多个线程的程序可以分配给多个内核,从而使事情以真正并发的方式发生。因此,将工作分配给多个线程可以使程序在多核 CPU 上运行得更快。

1.3、GUI 线程和工作线程

Qt GUI 必须在主线程中运行。所有QWidget和几个相关的类,例如 QPixmap,在辅助线程中不起作用。辅助线程通常被称为“工作线程”。

1.4、同时访问数据

每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块如何在内存中定位。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个单独的堆栈。

如果两个线程具有指向同一个对象的指针,则两个线程可能会同时访问该对象,这可能会破坏对象的完整性。很容易想象当同一个对象的两个方法同时执行时会出现很多问题。

有时需要从不同的线程访问一个对象。例如,当存在于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程交换数据比进程更容易便捷。数据不必序列化和复制。可以传递指针,但必须严格协调哪个线程接触哪个对象。必须防止对一个对象同时执行操作。

二、使用线程

线程基本上有两种使用场景:

  • 需要使用多核处理器加快处理速度。
  • 需要处理耗时操作但要保持 GUI 线程。

2.1、线程的替代方案

开发人员需要非常小心使用线程。启动线程很容易,但很难确保所有共享数据保持一致。问题通常很难发现,因为它们可能只偶尔出现一次或仅在特定的硬件配置上出现。在使用解决某些问题之前,应该考虑可能的替代方案。

  1. QEventLoop::processEvents():在耗时计算期间重复调用事件循环处理程序可防止 GUI 阻塞。但是,此解决方案不能很好地扩展,因为对 processEvents() 的调用可能发生得太频繁,也可能不够频繁,具体取决于硬件。
  2. QTimer:有时可以使用计时器方便地完成后台处理。
  3. QSocketNotifier
    QNetworkAccessManagerQIODevice::readyRead():(网络操作)这是拥有一个或多个线程的替代方案,每个线程在慢速网络连接上阻塞读取。只要响应一大块网络数据的计算可以快速执行,这种反应式设计比线程中的同步等待更好。响应式设计比线程更不容易出错。在许多情况下,还有性能优势。

QtConcurrent 模块提供了一个简单的接口,用于将工作分配给所有处理器的内核。线程代码完全隐藏在 QtConcurrent 框架中,因此不必关心细节。但是,当需要与正在运行的线程进行通信时,不能使用 QtConcurrent,也不应该使用它来处理阻塞操作

三、类列表

四、Qt中的多线程技术

Qt 提供了许多用于处理线程的类和函数。下面是 Qt 程序员可以用来实现多线程应用程序的四种不同方法。

4.1、QThread:具有可选事件循环的低级 API

QThread 是 Qt 中所有线程控制的基础。 每个 QThread 实例代表并控制一个线程。

QThread 可以直接实例化或子类化。实例化 QThread 提供了一个并行事件循环,允许在辅助线程中调用 QObject 槽函数。子类化 QThread 允许应用程序在启动其事件循环之前初始化新线程,或者在没有事件循环的情况下运行并行代码。

4.2、QThreadPool 和 QRunnable:重用线程

频繁地创建和销毁线程可能会很昂贵。为了减少这种开销,可以将现有线程重用于新任务。QThreadPool 是可重用 QThread 的集合。

要在 QThreadPool 的线程之一中运行代码,请重新实现 QRunnable::run() 并实例化子类 QRunnable。使用 QThreadPool::start() 将 QRunnable 放入 QThreadPool 的运行队列中。当一个线程可用时, QRunnable::run() 中的代码将在该线程中执行。

每个 Qt 应用程序都有一个全局线程池,可以通过 QThreadPool::globalInstance() 访问。 这个全局线程池会根据 CPU 中的内核数自动维护最佳线程数。 但是,可以显式创建和管理单独的 QThreadPool。

4.3、Qt Concurrent:使用高级 API

Qt Concurrent 模块提供了处理一些常见并行计算模式的高级函数:map、filter 和 reduce。与使用 QThread 和 QRunnable 不同,这些函数从不需要使用低级线程原语,例如互斥体或信号量。相反,它们返回一个 QFuture 对象,该对象可用于在函数准备好时检索函数的结果。QFuture 还可用于查询计算进度和暂停/恢复/取消计算。为方便起见,QFutureWatcher 支持通过信号和槽与 QFuture 进行交互。

Qt Concurrent 的映射、过滤和缩减算法会自动在所有可用的处理器内核之间分配计算。

该模块还提供了 QtConcurrent::run() 函数,它可以在另一个线程中运行任何函数。但是,QtConcurrent::run() 仅支持 map、filter 和 reduce 函数可用的功能子集。QFuture 可用于检索函数的返回值并检查线程是否正在运行。但是,对 QtConcurrent::run() 的调用仅使用一个线程,无法暂停/恢复/取消,也无法查询进度。

4.4、WorkerScript:QML 中的线程

WorkerScript QML 类型允许 JavaScript 代码与 GUI 线程并行运行。

每个 WorkerScript 实例都可以附加一个 .js 脚本。当 WorkerScript.sendMessage() 被调用时,脚本将在单独的线程(和单独的 QML 上下文)中运行。当脚本完成运行时,它可以将回复发送回 GUI 线程,该线程将调用 WorkerScript.onMessage() 信号处理程序。

使用 WorkerScript 类似于使用已移动到另一个线程的 QObject。数据通过信号在线程之间传输。

4.5、以上各方法比较

特点

QThread

QRunnable 和 QThreadPool

QtConcurrent::run()

Qt Concurrent

(Map, Filter, Reduce)

WorkerScript

语言

C++

C++

C++

C++

QML

可以指定线程优先级

Yes

Yes

可以运行事件循环

Yes

可以通过信号接收数据更新

Yes(QObject接收信号) 

Yes(WorkerScript 接收信号)

可以使用信号控制

Yes(QThread接收信号)

Yes(QFutureWatcher 接收信号)

可以通过 QFuture 监控

部分

Yes

内置暂停/恢复/取消功能

Yes

4.6、使用示例

线程的生命周期操作解决方案
一次性在另一个线程中运行一个新的线性函数,可在运行期间更新进度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 子类化以创建worker对象。实例化这个worker对象和一个 QThread。将工作线程移动到新线程。通过队列方式连接的信号槽向worker对象发送命令或数据。
永久在另一个线程中重复执行昂贵的操作,该线程不需要接收任何信号或事件直接在 QThread::run() 的重新实现中编写无限循环。在没有事件循环的情况下启动线程。让线程发出信号以将数据发送回 GUI 线程。

五、同步线程

虽然线程的目的是让代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程同时尝试写入同一个变量,结果是不确定的。Qt 提供了用于同步线程的低级原语和高级机制。 

5.1、低级同步原语

QMutex 是强制互斥量的基本类。一个线程锁定一个互斥量以获取对共享资源的访问。如果第二个线程在互斥已经被锁定的情况下试图锁定它,第二个线程将被置于睡眠状态,直到第一个线程完成其任务并解锁互斥锁。

QReadWriteLock 与 QMutex 类似,只是它区分了“读”和“写”访问。当一块数据没有被写入时,多个线程同时读取是安全的。QReadWriteLock 允许同时读取共享数据,从而提高并行性。

QSemaphore 是 QMutex 的泛化,它保护一定数量的相同资源。相比之下,QMutex 只保护一个资源。

QWaitCondition 不是通过强制互斥而是通过提供条件变量来同步线程。虽然其他原语使线程等待资源解锁,但 QWaitCondition 使线程等待直到满足特定条件。要允许等待的线程继续进行,请调用wakeOne() 唤醒一个随机选择的线程或wakeAll () 同时唤醒它们。

5.1.1、风险

如果一个线程锁定了一个资源但没有解锁它,应用程序可能会冻结,因为其他线程将永久无法使用该资源。例如,如果抛出异常并强制当前函数返回而不释放其锁,就会发生这种情况。

另一个类似的场景是死锁,例如,假设线程 A 正在等待线程 B 解锁资源,如果线程 B 也在等待线程 A 解锁不同的资源,那么两个线程将永远等待,因此应用程序将冻结。

5.1.2、便利类

QMutexLockerQReadLocker QWriteLocker 是便利类,它们使 QMutex 和 QReadWriteLock 的使用更容易。它们在构造时锁定资源,并在销毁时自动解锁。它们旨在简化使用 QMutex 和 QReadWriteLock 的代码,从而减少资源被意外永久锁定的可能性。

5.2、高级事件队列 

Qt 的事件系统对于线程间通信非常有用。每个线程可能有自己的事件循环。要调用另一个线程中的槽函数(或任何可调用方法),请将调用放在目标线程的事件循环中。这让目标线程在槽开始运行之前完成其当前任务,而原始线程继续并行运行。

要将调用置于事件循环中,请建立一个Qt::QueuedConnection类型的信号槽连接。每当发出信号时,其参数将被事件系统记录。信号接收者所在的线程将运行该槽。或者,调用QMetaObject::invokeMethod() 不用信号也能达到同样的效果,两种情况都必须使用Qt::QueuedConnection连接,因为直接连接绕过了事件系统,会在当前线程中立即运行该方法。
与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统不强制互斥。如果可调用方法访问共享数据,它们仍然必须用低级原语保护。

话虽如此,Qt 的事件系统,连同隐式共享的数据结构,提供了传统线程锁定的替代方案。如果信号和槽被独占使用,并且线程之间不共享变量,则多线程程序可以完全没有低级原语。

六、可重入和线程安全的概念

6.1、可重入函数和线程安全函数

  • 可重入函数:函数可以同时被多个线程调用,但是每个调用者只能使用自己的数据,而不能使用共享数据。
  • 线程安全函数:函数可以同时被多个线程调用,调用者可以使用共享数据,共享数据的使用是串行的,即一个线程使用时完全占用共享数据,用完了再由第二个线程完全占用使用。

6.2、可重入类和线程安全类

  • 可重入类,它的成员函数可以被多个线程安全地调用,只要每个线程使用这个类的不同的对象。
  • 线程安全类,它的成员函数能够被多线程安全地调用,即使所有的线程都使用该类的同一个实例也没有关系。

因此,线程安全函数总是可重入的,但可重入函数并不总是线程安全的。

七、QObject和线程

QThread 继承了 QObject,它发出信号来指示线程开始或完成执行。QObjects 可以在多个线程中使用,发出调用其他线程中的槽的信号。

7.1、QObject 重入

QObject 是可重入的。它的大多数非 GUI 子类,例如 QTimer、QTcpSocket、QUdpSocket 和 QProcess 也是可重入的,使得可以同时从多个线程使用这些类。注意这些类是设计为从创建和使用在单个线程中。

在一个线程中创建对象并从另一个线程调用其函数不能保证工作。需要注意三个约束:

  • QObject 的子对象必须始终在创建父对象的线程中创建。
  • 事件驱动的对象只能在单线程中使用,具体来说,这适用于定时器机制和网络模块。如定时器或Socket不能在线程A创建而在线程B中启动。
  • 必须确保在删除 QThread 之前删除在线程中创建的所有对象。

尽管 QObject 是可重入的,但 GUI 类,尤其是 QWidget 及其所有子类是不可重入的。它们只能在主线程中使用。QCoreApplication::exec() 也必须从该线程调用。

通常,不支持在 QApplication 之前创建 QObjects,因为可能导致退出时奇怪的崩溃,具体取决于平台。这意味着也不支持 QObject 的静态实例。结构合理的单线程或多线程应用程序应该使 QApplication 成为第一个创建,最后一个销毁 QObject。

7.2、线程的事件循环

每个线程都可以有自己的事件循环。初始线程使用 QCoreApplication::exec() 启动其事件循环,或者对于单对话框 GUI 应用程序,有时使用 QDialog::exec()。其他线程可以使用 QThread::exec() 启动事件循环。和QCoreApplication一样,QThread提供了一个exit(int)函数和一个quit()函数。

QObject 对象存在于创建它的线程中。该对象的事件由该线程的事件循环调度。使用 QObject::thread() 可以获取QObject 所在的线程。

QObject::moveToThread() 函数更改对象及其子对象的线程(如果对象有父对象,则不能移动对象)。

在拥有该对象的线程以外的线程调用 QObject 上的 delete 是不安全的,除非保证该对象在那一刻不处理事件。使用 QObject :: deleteLater ()则可以安全删除对象,此函数会发布一个 DeferredDelete 事件,该对象的线程的事件循环最终会在处理池事件时删除对象。

可以随时使用线程安全函数 QCoreApplication::postEvent() 手动将事件发布到任何线程中的任何对象。事件将由创建该对象的线程的事件循环自动调度。

所有线程都支持事件过滤器,限制是监控对象必须和被监控对象在同一个线程中。类似的,QCoreApplication::sendEvent()只能用于将事件分派给在调用函数的线程中存活的对象。

7.3、从其他线程访问 QObject 子类

QObject 及其所有子类都不是线程安全的。整个事件传递系统也不是线程安全的。

如果您在不存在于当前线程中的 QObject 子类上调用函数并且该对象可能接收事件,则必须使用互斥锁保护对 QObject 子类内部数据的所有访问,否则,可能会遇到崩溃或其他不希望的情况行为。

与其他对象一样,QThread 对象存在于创建对象的线程中,而不是在调用 QThread::run() 时创建的线程中。在 QThread 子类中提供槽函数通常是不安全的,除非保护带有互斥锁的成员变量。

另一方面,可以安全地从 QThread::run () 实现中发出信号,因为信号发出是线程安全的

7.4、跨线程的信号和槽

信号和槽

QObject::connect() 本身是线程安全的。

八、Qt 模块中的线程支持

8.1、线程和 SQL 模块

连接只能在创建它的线程内使用。不支持在线程之间移动连接或从不同线程创建查询。

8.2、线程中绘制

QPainter 可以在线程中用于在 QImage、QPrinter 、QPicture 绘画设备上绘画。不支持在 QPixmaps 和 QWidgets 上绘画。

在给定的绘制设备上一次只能有一个线程绘制。

8,3、线程和富文本处理

QTextDocument、QTextCursor 和所有相关的类都是可重入的。

8.4、线程和 SVG 模块

QtSvg 模块中的 QSvgGenerator 和 QSvgRenderer 类是可重入的。

8.5、线程和隐式共享类

Qt 对其许多值类使用称为隐式共享的优化,特别是 QImage 和 QString。隐式共享类可以安全地跨线程复制。它们是完全可重入的。

在很多人的印象中,隐式共享和多线程是不兼容的概念,因为引用计数通常是这样做的,但是 Qt 使用原子引用计数来确保共享数据的完整性,避免引用计数器的潜在损坏。

请注意,原子引用计数不保证线程安全。在线程之间共享隐式共享类的实例时应使用适当的锁定。这与所有可重入类的要求相同。但是,建议使用信号和槽在线程之间传递数据,因为这无需任何显式锁定即可完成。

隐式共享类确实是隐式共享的,即使在多线程应用程序中,也可以安全地使用它们,就好像它们是普通的、非共享的、可重入的基于值的类一样。

  • 1
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Qt是一种跨平台的应用程序开发框架,提供了丰富的工具和功能,可以用于开发各种类型的应用程序。为了方便开发者使用和学习QtQt官方提供了详尽的帮助文档。 Qt帮助文档中文版PDF是Qt官方提供的Qt帮助文档的中文翻译版本,以PDF的形式提供给开发者。这个PDF文档包含了Qt框架的全部相关内容,包括Qt概述、安装配置、使用指南、示例代码、API参考等等。 Qt帮助文档中文版PDF的好处是,开发者可以将其下载到本地,方便离线查阅。这个PDF文档结构清晰,内容全面,对于初学者来说非常友好,可以帮助开发者快速入门和掌握Qt的使用。 Qt帮助文档中文版PDF中的内容涵盖了Qt框架的各个方面,包括图形界面设计、信号槽机制、文件读写、多线程编程等等,涉及到的知识点非常全面。通过阅读该文档,开发者可以了解Qt的各个模块和功能的用法,并可以根据需要进行深入学习和研究。 总的来说,Qt帮助文档中文版PDF是Qt开发者不可或缺的学习资料之一。它提供了官方的中文翻译版本,对于那些初学者和非英语母语的开发者来说,是一个非常有价值的资源。无论是入门学习还是深入研究Qt,都可以通过阅读Qt帮助文档中文版PDF来快速掌握相关知识和技巧。 ### 回答2: Qt 是一款流行的跨平台应用程序开发框架,其官方提供了详细的帮助文档。这些帮助文档包含了 Qt 框架的所有模块和功能的介绍,对于开发者来说非常重要。其中就包括了 Qt 帮助文档的中文版 PDF。 Qt 帮助文档中文版 PDF 是由 Qt 官方团队经过翻译并编制而成的。这个 PDF 版本与在线帮助文档相似,但可以离线查看,非常方便。这对于有限的网络连接或希望在没有网络的环境中使用 Qt 帮助文档的开发者非常有用。 Qt 帮助文档中文版 PDF 提供了详细的 Qt 框架说明,包括 Qt 的核心模块,如图形界面、网络通信、数据库等。每个模块都有详细的文档,其中包含了类的描述、函数的用法和示例代码。这些文档提供了对 Qt 框架的全面理解和实践指导,可以帮助开发者更高效地使用 Qt 进行应用程序开发。 Qt 帮助文档中文版 PDF 为开发者提供了一个快速查找和学习 Qt 框架的工具。无论是初学者还是有经验的开发者,都可以从这些文档中受益。开发者可以方便地通过关键字搜索相关主题,找到他们需要的信息。此外,它还提供了书签和目录,以帮助开发者更好地组织和浏览文档内容。 总而言之,Qt 帮助文档中文版 PDF 是 Qt 开发者的重要资源之一。它提供了对 Qt 框架的全面了解和使用指南,可帮助开发者更高效地创建跨平台的应用程序。无论是初学者还是有经验的开发者,都可以从中获得所需的知识和指导。 ### 回答3: Qt是一种跨平台的应用程序开发框架,提供了丰富的功能和工具用于开发图形界面和跨平台应用程序。Qt官方提供了完整而详细的帮助文档,其中包括了Qt框架的所有功能和用法说明。 Qt帮助文档中文版PDF是针对中文开发者的翻译版本,使得使用中文进行开发的开发者可以更方便地查阅和理解Qt的相关知识。这份PDF文档包含了Qt框架各个模块的详细说明,如界面设计、信号和槽机制、文件操作等。开发者可以通过它来了解Qt框架的整体结构和每个模块的使用方法。 Qt帮助文档中文版PDF的优势在于,它使用中文进行了翻译,避免了开发者因为英文文档的障碍而产生的理解困难。无论是初学者还是有一定经验的开发者,通过这份中文版PDF文档,都能更加轻松地学习和使用Qt框架。 此外,Qt帮助文档中文版PDF还可以离线使用,方便开发者在没有网络连接的情况下进行查阅和学习。无论是在学习阶段还是在实际开发过程中遇到问题,开发者只需打开PDF文档,便可方便快捷地找到所需信息。 总而言之,Qt帮助文档中文版PDF是一个便捷且易于使用的资源,它为中文开发者提供了全面的Qt框架使用说明,有助于开发者更好地理解和应用Qt框架,提高开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值