网站首页 > 技术文章 正文
一、关于线程
线程的用途:单个进程内的并发。
1.1、单核CPU
在单核 CPU 上并行工作是一种错觉。对于进程,这种错觉是通过在很短的时间后中断处理器对一个进程的工作而产生的。然后处理器继续下一个过程。为了在进程之间切换,保存当前程序计数器并加载下一个处理器的程序计数器;对寄存器和某些体系结构和操作系统特定数据进行处理。
正如一个 CPU 可以驱动两个或多个进程一样,也可以让 CPU 在一个进程的两个不同代码段上运行。当一个进程启动时,它总是执行一个代码段,该进程被称为主线程。程序可能会决定启动第二个线程。然后,在一个进程中同时处理两个不同的代码序列。在单核 CPU 上通过重复保存程序计数器和寄存器然后加载下一个线程的程序计数器和寄存器来实现并发。在活动线程之间循环不需要程序的合作。当切换到下一个线程时,一个线程可能处于任何状态。
1.2、多核CPU
CPU 设计的当前趋势是具有多个内核。具有多个线程的程序可以分配给多个内核,从而使事情以真正并发的方式发生。因此,将工作分配给多个线程可以使程序在多核 CPU 上运行得更快。
1.3、GUI 线程和工作线程
Qt GUI 必须在主线程中运行。所有QWidget和几个相关的类,例如 QPixmap,在辅助线程中不起作用。辅助线程通常被称为“工作线程”。
1.4、同时访问数据
每个线程都有自己的堆栈,这意味着每个线程都有自己的调用历史和局部变量。与进程不同,线程共享相同的地址空间。下图显示了线程的构建块如何在内存中定位。非活动线程的程序计数器和寄存器通常保存在内核空间中。每个线程都有一个共享的代码副本和一个单独的堆栈。
如果两个线程具有指向同一个对象的指针,则两个线程可能会同时访问该对象,这可能会破坏对象的完整性。很容易想象当同一个对象的两个方法同时执行时会出现很多问题。
有时需要从不同的线程访问一个对象。例如,当存在于不同线程中的对象需要通信时。由于线程使用相同的地址空间,线程交换数据比进程更容易便捷。数据不必序列化和复制。可以传递指针,但必须严格协调哪个线程接触哪个对象。必须防止对一个对象同时执行操作。
二、使用线程
线程基本上有两种使用场景:
需要使用多核处理器加快处理速度。
需要处理耗时操作但要保持 GUI 线程。
2.1、线程的替代方案
开发人员需要非常小心使用线程。启动线程很容易,但很难确保所有共享数据保持一致。问题通常很难发现,因为它们可能只偶尔出现一次或仅在特定的硬件配置上出现。在使用解决某些问题之前,应该考虑可能的替代方案。
QEventLoop::processEvents():在耗时计算期间重复调用事件循环处理程序可防止 GUI 阻塞。但是,此解决方案不能很好地扩展,因为对 processEvents() 的调用可能发生得太频繁,也可能不够频繁,具体取决于硬件。
QTimer:有时可以使用计时器方便地完成后台处理。
QSocketNotifier、
QNetworkAccessManager、QIODevice::readyRead():(网络操作)这是拥有一个或多个线程的替代方案,每个线程在慢速网络连接上阻塞读取。只要响应一大块网络数据的计算可以快速执行,这种反应式设计比线程中的同步等待更好。响应式设计比线程更不容易出错。在许多情况下,还有性能优势。
QtConcurrent 模块提供了一个简单的接口,用于将工作分配给所有处理器的内核。线程代码完全隐藏在 QtConcurrent 框架中,因此不必关心细节。但是,当需要与正在运行的线程进行通信时,不能使用 QtConcurrent,也不应该使用它来处理阻塞操作。
三、类列表
- QAtomicInteger:Qt提供的原子操作
- QAtomicPointer:Qt提供的原子操作
- QFuture:异步运行结果
- QFutureSynchronizer:简化了QFuture的同步
- QFutureWatcher:监视QFuture
- QMutex:互斥量,使一段代码在一段时间内只能由一个线程访问
- QMutexLocker:互斥锁,关联QMutex对象。创建时锁定、销毁时解锁QMutex对象
- QReadLocker:读取锁,用作简化 QReadWriteLock 的读取
- QReadWriteLock:读写锁
- QRecursiveMutex:互斥量,同一线程内可以多次调用 lock()
- QRunnable:线程池中的线程要执行的内容
- QSemaphore:信号量
- QSemaphoreReleaser:确保安全释放信号量
- QThread:管控一个线程
- QThreadPool:线程池
- QThreadStorage:一个模板类,可以为每个线程储存指定类型的数据
- QWaitCondition:线程等待条件
- QWriteLocker:写入锁,用作简化 QReadWriteLock 的写入
- QtConcurrent:Qt并发命名空间
四、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、以上各方法比较
4.6、使用示例
虽然线程的目的是让代码并行运行,但有时线程必须停止并等待其他线程。例如,如果两个线程同时尝试写入同一个变量,结果是不确定的。Qt 提供了用于同步线程的低级原语和高级机制。
5.1、低级同步原语
QMutex 是强制互斥量的基本类。一个线程锁定一个互斥量以获取对共享资源的访问。如果第二个线程在互斥已经被锁定的情况下试图锁定它,第二个线程将被置于睡眠状态,直到第一个线程完成其任务并解锁互斥锁。
QReadWriteLock 与 QMutex 类似,只是它区分了“读”和“写”访问。当一块数据没有被写入时,多个线程同时读取是安全的。QReadWriteLock 允许同时读取共享数据,从而提高并行性。
QSemaphore 是 QMutex 的泛化,它保护一定数量的相同资源。相比之下,QMutex 只保护一个资源。
QWaitCondition 不是通过强制互斥而是通过提供条件变量来同步线程。虽然其他原语使线程等待资源解锁,但 QWaitCondition 使线程等待直到满足特定条件。要允许等待的线程继续进行,请调用wakeOne() 唤醒一个随机选择的线程或wakeAll () 同时唤醒它们。
5.1.1、风险
如果一个线程锁定了一个资源但没有解锁它,应用程序可能会冻结,因为其他线程将永久无法使用该资源。例如,如果抛出异常并强制当前函数返回而不释放其锁,就会发生这种情况。
另一个类似的场景是死锁,例如,假设线程 A 正在等待线程 B 解锁资源,如果线程 B 也在等待线程 A 解锁不同的资源,那么两个线程将永远等待,因此应用程序将冻结。
5.1.2、便利类
QMutexLocker、QReadLocker 、 QWriteLocker 是便利类,它们使 QMutex 和 QReadWriteLock 的使用更容易。它们在构造时锁定资源,并在销毁时自动解锁。它们旨在简化使用 QMutex 和 QReadWriteLock 的代码,从而减少资源被意外永久锁定的可能性。
5.2、高级事件队列
Qt 的事件系统对于线程间通信非常有用。每个线程可能有自己的事件循环。要调用另一个线程中的槽函数(或任何可调用方法),请将调用放在目标线程的事件循环中。这让目标线程在槽开始运行之前完成其当前任务,而原始线程继续并行运行。
要将调用置于事件循环中,请建立一个Qt::QueuedConnection类型的信号槽连接。每当发出信号时,其参数将被事件系统记录。信号接收者所在的线程将运行该槽。或者,调用QMetaObject::invokeMethod() 不用信号也能达到同样的效果,两种情况都必须使用Qt::QueuedConnection连接,因为直接连接绕过了事件系统,会在当前线程中立即运行该方法。
与使用低级原语不同,使用事件系统进行线程同步时没有死锁的风险。但是,事件系统不强制互斥。如果可调用方法访问共享数据,它们仍然必须用低级原语保护。
话虽如此,Qt 的事件系统,连同隐式共享的数据结构,提供了传统线程锁定的替代方案。如果信号和槽被独占使用,并且线程之间不共享变量,则多线程程序可以完全没有低级原语。
【领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 使用原子引用计数来确保共享数据的完整性,避免引用计数器的潜在损坏。
请注意,原子引用计数不保证线程安全。在线程之间共享隐式共享类的实例时应使用适当的锁定。这与所有可重入类的要求相同。但是,建议使用信号和槽在线程之间传递数据,因为这无需任何显式锁定即可完成。
隐式共享类确实是隐式共享的,即使在多线程应用程序中,也可以安全地使用它们,就好像它们是普通的、非共享的、可重入的基于值的类一样。
【领QT开发教程学习资料,点击下方链接莬费领取↓↓,先码住不迷路~】
点击这里:「链接」
原文链接:https://blog.csdn.net/kenfan1647/article/details/118487513
猜你喜欢
- 2024-10-24 QT(17)- QNetworkAccessManager qnetworkinterface.allinterfaces
- 2024-10-24 Qt多线程的三种方法QThread qt多线程直接处理数据
- 2024-10-24 从零开始学Qt(86):TCP服务器端程序设计
- 2024-10-24 Qt Core学习日记——第九天QObjectData
- 2024-10-24 Qt智能指针--QSharedPointer qt智能指针.get和.data函数后计数会加吗
- 2024-10-24 QT(11)- QThread qt ui thread work thread
- 2024-10-24 Qt QVariant的用法 qt基本语法
- 2024-10-24 Qt5中QOverload的用法 qt5coredll
- 2024-10-24 Qt多线程编程之QThread qt中的多线程
- 2024-10-24 Qt QModbusReply类 qty是什么单位的缩写
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)