当前位置: 首页 > news >正文

我的Qt八股文笔记2:Qt并发编程方案对比与QPointer,智能指针方案

Qt 并发编程:方案分析与对比

Qt 提供了多种并发编程方案,每种方案都有其优势和适用场景。


1. 线程(QThread

方案描述:

QThread 是 Qt 提供的低级线程抽象。它允许你创建和管理独立的执行流。通常,你不会直接在 QThread 子类中重写 run() 方法来执行任务(尽管这在旧版或简单场景中可以),而是将一个 QObject 子类的实例移动到 QThread 实例中,让该 QObject 来执行任务。这种方式更符合 Qt 的事件驱动模型。

工作原理:

  1. 创建一个 QThread 实例。
  2. 创建一个 QObject 派生类的实例(我们称之为 worker 对象),它包含要执行的耗时任务,这些任务可以作为槽函数。
  3. 调用 worker->moveToThread(threadInstance) 将 worker 对象移动到新的线程。
  4. 通过 QObject::connect() 连接信号和槽,例如将 QThreadstarted() 信号连接到 worker 对象的某个任务槽,或将 worker 对象的完成信号连接回主线程的槽。
  5. 调用 threadInstance->start() 启动线程。

优势:

  • 完全控制: 提供了对线程生命周期和行为的精细控制。
  • 通用性: 适用于各种复杂的并发场景,可以执行任何类型的耗时操作。
  • 与其他 Qt 机制良好集成: 可以方便地与信号与槽结合,实现线程间安全通信。

劣势:

  • 复杂性高: 需要手动管理线程的创建、启动、停止和销毁,以及线程间的通信和同步,容易出错。
  • 资源消耗: 创建和管理线程的开销相对较大。
  • 易引入 bug: 不当的线程同步可能导致死锁、竞态条件等问题。

适用场景:

  • 需要长期运行的后台任务。
  • 复杂的数据处理或计算密集型任务。
  • 需要与系统底层线程 API 紧密交互的场景。

2. 线程池(QThreadPoolQRunnable

方案描述:

QThreadPool 提供了一个管理线程集合的机制,避免了频繁创建和销毁线程的开销。你将任务封装成 QRunnable 实例,然后将其提交给线程池。线程池会自动从池中分配一个可用线程来执行任务。

工作原理:

  1. 创建一个继承自 QRunnable 的类,并重写其 run() 方法,在该方法中实现要执行的任务。
  2. 创建 QRunnable 子类的实例。
  3. 通过 QThreadPool::globalInstance()->start(runnableInstance) 将任务提交给全局线程池。你也可以创建自定义的 QThreadPool 实例。

优势:

  • 资源高效: 重用线程,减少了线程创建和销毁的开销。
  • 管理简化: 线程的生命周期和调度由线程池负责,开发者只需关注任务本身。
  • 并发控制: 可以设置线程池的最大线程数,避免过度创建线程导致系统资源耗尽。

劣势:

  • 通信限制: QRunnable 本身不是 QObject,不能直接使用信号与槽进行通信。如果需要通信,通常需要将 QRunnable 作为 QObject 的成员,或在 QRunnable 内部使用 QMetaObject::invokeMethod 将结果发送回主线程。
  • 任务粒度: 适合执行独立的、短期的任务。不适合需要长期运行或频繁与主线程交互的任务。

适用场景:

  • 大量独立的、计算密集型或 I/O 密集型任务。
  • 例如,图片处理、文件批量上传下载、网络请求等。

3. QtConcurrent 模块

方案描述:

QtConcurrent 模块提供了一组高级 API,用于执行并行操作,通常不需要直接接触线程。它构建在 QThreadPool 之上,提供了更简洁的方式来处理常见的并发模式,如映射(map)、过滤(filter)和归约(reduce)。

工作原理:

QtConcurrent 提供了一些便利函数,例如:

  • QtConcurrent::run():在一个单独的线程中执行一个函数。这是最常用的,相当于将一个可调用对象(函数、Lambda)提交给 QThreadPool
  • QtConcurrent::map() / QtConcurrent::mapped():对容器中的每个元素并行应用一个函数。
  • QtConcurrent::filter() / QtConcurrent::filtered():并行过滤容器中的元素。
  • QtConcurrent::reduce() / QtConcurrent::reduced():并行对容器中的元素进行归约操作。

优势:

  • 简单易用: 提供了函数式编程风格的接口,代码量少,易于理解。
  • 自动管理: 线程管理、任务调度和结果收集都由 QtConcurrent 自动完成。
  • 高效: 底层使用 QThreadPool,效率高。

劣势:

  • 功能受限: 适用于特定的并行模式(如 map/reduce),对于更复杂的线程交互或长期运行任务,可能不如 QThread 灵活。
  • 结果获取: 通常通过 QFuture 对象来获取操作的结果,需要处理 QFutureWatcher 或轮询 isFinished()

适用场景:

  • 对集合数据进行并行处理。
  • 执行一次性的、无需复杂线程间通信的后台函数。
  • 例如,图像滤镜应用、大规模数据统计、文件搜索等。

4. QFutureQFutureWatcher

方案描述:

QFuture 代表一个异步操作的结果,而 QFutureWatcher 允许你监控 QFuture 的状态(例如,是否完成、进度、是否取消),并以信号和槽的方式通知你。它们通常与 QtConcurrent 或其他异步操作一起使用。

工作原理:

  1. 调用 QtConcurrent::run() 或其他异步函数,它们会返回一个 QFuture 对象。
  2. 创建一个 QFutureWatcher 实例。
  3. QFuture 关联到 QFutureWatcher (watcher.setFuture(future) )。
  4. 连接 QFutureWatcher 的信号(如 finished(), progressed(), canceled())到主线程的槽,以便在操作完成或状态变化时得到通知。

优势:

  • 异步结果通知: 提供了非阻塞地获取异步操作结果的机制。
  • 进度报告: 可以方便地报告任务进度。
  • 取消操作: 支持取消正在进行的异步任务。

劣势:

  • 不能独立使用: 它们本身不执行并发操作,而是用于管理和监控其他并发方案(如 QtConcurrent)的执行结果。

适用场景:

  • 任何需要异步获取结果、监控进度或取消操作的并发任务。
  • QtConcurrent 结合使用时,是获取结果和更新 UI 的标准方式。

5. QObject::invokeMethod (队列连接)

方案描述:

虽然不是独立的并发方案,但 QObject::invokeMethod 在结合 Qt::QueuedConnection 时,是跨线程安全调用槽函数的重要方式,对于理解 Qt 并发通信至关重要。

工作原理:

当你在一个线程中调用属于另一个线程的 QObject 的槽时,如果使用 Qt::QueuedConnection 或 Qt::AutoConnection(并且识别出是跨线程调用),Qt 会将这个方法调用封装成一个事件,放入目标线程的事件队列中。目标线程的事件循环处理到这个事件时,才会执行相应的槽函数。

优势:

  • 线程安全通信: 确保槽函数在正确的(接收者)线程中执行,避免了数据竞争。
  • 简单易用: API 相对直观。

劣势:

  • 间接性: 不是直接的函数调用,存在少量延迟。
  • 参数限制: 传递的参数必须是 Qt 的元对象系统已知的类型(Q_DECLARE_METATYPE 宏注册)。

适用场景:

  • 所有跨线程的信号与槽通信。
  • 在工作线程中完成任务后,向主线程发送结果或更新 UI。

方案对比与选择建议

方案复杂性资源消耗线程控制任务类型线程安全通信典型应用场景
QThread中等精细长期/复杂需手动同步后台服务、复杂数据处理、I/O 密集型任务
QThreadPool中等池化管理独立/短期间接通信大量并发计算、文件批量处理、网络请求
QtConcurrent自动集合操作/一次性QFutureWatcher并行数据处理 (map/filter/reduce)、后台函数执行
QFuture & QFutureWatcher监控异步结果获取结果通知监控 QtConcurrent 或其他异步任务的进度和结果
QObject::invokeMethod队列跨线程调用安全通信所有跨线程的信号与槽调用

选择建议:

  • 最常用和推荐的模式: 优先考虑使用 QtConcurrent::run() 来执行简单的后台任务,并通过 QFuture + QFutureWatcher 将结果安全地回调到主线程更新 UI。
  • 大量独立任务: 如果有大量独立的、可并行执行的任务,考虑使用 QThreadPool 管理 QRunnable
  • 复杂或长期运行的任务:QtConcurrent 无法满足需求,或者需要对线程生命周期有更精细的控制时,再考虑使用 QThreadQObject 移动到新线程中执行。记住 不要直接继承 QThread 来实现任务
  • 线程间通信: 无论选择哪种并发方案,始终使用 信号与槽(特别是队列连接)QObject::invokeMethod 来实现线程间的安全通信,避免直接访问共享数据。

理解这些方案及其优缺点,能帮助你选择最适合你应用程序需求的并发编程方法,从而构建响应迅速、稳定高效的 Qt 应用。


QPointer 与标准库智能指针:有何不同?

在 C++ 中,智能指针是管理动态内存的强大工具,它们通过 RAII(资源获取即初始化)原则自动处理内存的分配和释放,从而帮助避免内存泄漏。Qt 提供了它自己的智能指针类 QPointer,而 C++ 标准库则提供了 std::unique_ptrstd::shared_ptrstd::weak_ptr。虽然它们都旨在解决内存管理问题,但它们的目标、设计哲学和适用场景却大相径庭。


QPointer:专为 QObject 而生

QPointer 是一个模板类,设计用于安全地指向 QObject 及其派生类的实例。它的核心能力是自动置空(nullification)

核心特性:

  • 只适用于 QObject QPointer 只能管理继承自 QObject 的对象。这是因为它的实现依赖于 QObject 的元对象系统(Meta-Object System)和对象树(Object Tree)机制。
  • 自动置空(Nullification): 当它所指向的 QObject 对象被删除时(无论是通过 delete 显式删除,还是因为父对象被删除导致子对象被自动删除),QPointer 会自动将其内部的指针设置为 nullptr。这意味着你可以安全地检查 QPointer 是否仍然有效,而无需担心野指针。
  • 不拥有对象: QPointer 不负责管理对象的生命周期。它只是一个“观察者”指针,它的存在不会阻止被指向的对象被销毁。
  • 轻量级: 它比 std::shared_ptr 更轻量,因为它不需要引用计数。

适用场景:

  • 当你需要一个指向 QObject 的指针,但该对象可能在任何时候被其他代码删除(例如,UI 控件可能被用户关闭的窗口删除),并且你需要安全地检查指针的有效性时。
  • 避免对已删除 QObject 访问导致的崩溃(野指针)。

标准库智能指针:通用内存管理

std::unique_ptrstd::shared_ptrstd::weak_ptr 是 C++11 引入的智能指针,它们是通用的内存管理工具,可以管理任何类型的动态分配对象。

std::unique_ptr:独占所有权
  • 独占所有权: std::unique_ptr 确保在任何时候只有一个智能指针拥有所管理的对象。当 unique_ptr 超出作用域时,它所指向的对象会被自动删除。
  • 轻量高效: 它的开销与原始指针几乎相同,因为没有引用计数。
  • 不可复制,可移动: unique_ptr 不能被复制,但可以通过移动语义(std::move)转移所有权。

适用场景:

  • 当对象有且只有一个所有者时。
  • 工厂函数返回新创建的堆对象。
  • 作为类成员,管理其独占的资源。
std::shared_ptr:共享所有权
  • 共享所有权: std::shared_ptr 允许多个智能指针共享同一个对象的所有权。它通过引用计数来跟踪有多少个 shared_ptr 实例指向同一个对象。当最后一个 shared_ptr 超出作用域或被重置时,所管理的对象才会被删除。
  • 循环引用问题: shared_ptr 的一个主要缺点是可能导致循环引用,从而造成内存泄漏。当两个或多个 shared_ptr 相互引用时,它们的引用计数永远不会降为零,导致对象无法被销毁。

适用场景:

  • 当多个指针需要共享同一个对象的所有权,并且对象的生命周期由所有共享所有权的指针共同决定时。
  • 在数据结构中,多个节点需要引用同一个对象。
std::weak_ptr:观察者指针,解决循环引用
  • 非拥有性观察者: std::weak_ptr 是一种特殊的智能指针,它指向由 std::shared_ptr 管理的对象,但不增加对象的引用计数
  • 解决循环引用: weak_ptr 主要用于解决 shared_ptr 的循环引用问题。
  • 需要提升: 要访问 weak_ptr 所指向的对象,你需要先将其“提升”为 std::shared_ptr(通过 lock() 方法),如果对象已经不存在,lock() 会返回一个空的 shared_ptr

适用场景:

  • 打破 shared_ptr 之间的循环引用。
  • 缓存机制中,当缓存的对象可能被销毁时,可以作为对缓存对象的“弱引用”。
  • 观察者模式中,观察者持有对主题的弱引用,避免主题的生命周期被观察者影响。

QPointer 与标准库智能指针的本质区别

特性QPointer<T>std::unique_ptr<T>std::shared_ptr<T>std::weak_ptr<T>
管理对象类型仅限 QObject 及其派生类任意类型任意类型任意类型 (作为 shared_ptr 的观察者)
所有权无所有权(观察者)独占所有权共享所有权无所有权(观察者)
自动置空有(当 QObject 被销毁时)无(指针失效后,需要手动检查)无(指针失效后,需要手动检查)有(通过 lock() 检查是否过期)
内存管理不负责内存释放负责内存释放 (当自身销毁时)负责内存释放 (当引用计数为零时)不负责内存释放
用途安全引用 QObject,避免野指针独占资源管理,明确所有权共享资源管理,多所有者场景解决循环引用,非拥有性观察
开销轻量级极低,与原始指针类似较高(引用计数)较轻(不含引用计数,但需要 shared_ptr 配合)

总结

  • QPointer 是 Qt 特有的,用于解决 QObject 对象生命周期不确定性导致的野指针问题。 它的核心是“自动置空”特性,让你可以安全地检查 QObject 是否仍然存在。它不管理内存,只是一个安全的引用。
  • 标准库智能指针是通用的 C++ 内存管理工具。
    • std::unique_ptr 用于独占资源所有权,是原始指针的最佳替代。
    • std::shared_ptr 用于共享资源所有权,当多个对象需要共同管理一个资源的生命周期时使用。
    • std::weak_ptr 用于打破 std::shared_ptr 的循环引用,提供非拥有性的观察。

简而言之,当你处理 Qt 的 QObject 对象时,QPointer 是确保引用安全的首选。而当你处理非 QObject 的通用 C++ 动态内存时,则应根据所有权语义选择 std::unique_ptrstd::shared_ptr,并用 std::weak_ptr 来解决可能出现的循环引用。

http://www.dtcms.com/a/279112.html

相关文章:

  • 电气安全监测系统:筑牢电气安全防线
  • DAOS系统架构-Container
  • 壹[1],异步与多线程
  • 美联储降息趋缓叠加能源需求下调,泰国证券交易所新一代交易系统架构方案——高合规、强韧性、本地化的跨境金融基础设施解决方案
  • 【Linux】Ubuntu22.04安装zabbix
  • 固态金属储氢实用化提速:新氢动力 20 公斤级系统重磅发布
  • GaussDB in的用法
  • Linux部署Mysql
  • JavaScript进阶篇——第一章 作用域与垃圾回收机制
  • Netty编程模型介绍
  • 每天学习一个Python库之os库
  • Debezium日常分享系列之:Debezium 3.2.0.Final发布
  • MySQL Innodb Cluster配置
  • Ubuntu服务器安装Miniconda
  • VS2019编译使用log4cplus 1.2.0
  • AI数字人正成为医药行业“全场景智能角色”,魔珐科技出席第24届全国医药工业信息年会
  • DataWhale AI夏令营 Task2笔记
  • Linux —— A / 基础指令
  • 【牛客LeetCode数据结构】单链表的应用——合并两个有序链表问题、链表的回文结构问题详解
  • 游戏设备软件加密锁复制:技术壁垒与安全博弈
  • js与vue基础学习
  • 鸿蒙应用开发: 鸿蒙项目中使用私有 npm 插件的完整流程
  • docker-compose 安装Alist
  • Cesium源码打包
  • 数字孪生技术驱动UI前端革新:实现产品设计的虚拟仿真与实时反馈
  • Django Admin 配置详解
  • 【更新至2024年】2009-2024年上市公司华证esg评级、评分数据(含细分项)(年度+季度)
  • 大数据在UI前端的应用深化:基于用户行为数据的界面布局优化
  • 来时路,零帧起手到Oracle大师
  • Faiss能解决什么问题?Faiss是什么?