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

【QT】多线程相关教程

一、核心概念与 Qt 线程模型

1.线程与进程的区别:
线程是程序执行的最小单元,进程是资源分配的最小单元,线程共享进程的内存空间(堆,全局变量等),而进程拥有独立的内存空间。Qt线程只要关注同一进程内的并发。

2.为什么使用多线程
当程序中有多个耗时的操作时候,为了提高性能,防止GUI线程阻塞,可以处理耗时操作

3.QThread 类的相关接口
start():启动线程,调用run方法
run():线程的入口点,子类化 QThread 并重写此方法是一种使用线程的方式(传统方式)。线程在此函数中执行。
quit() / exit(int returnCode): 请求线程退出事件循环(如果正在运行)。
wait([unsigned long time = ULONG_MAX]): 阻塞调用线程,直到目标线程结束执行或超时。
isRunning() / isFinished(): 查询线程状态。
finished 信号:线程执行完毕(run() 返回)时发射。重要: 连接此信号进行资源清理(如 deleteLater)。
started 信号:线程启动后(run() 执行前)发射。

4.事件循环 (Event Loop)
核心概念: 每个线程都可以拥有自己的事件循环(由 QEventLoop 管理)。主线程(GUI 线程)默认运行事件循环。
作用: 处理事件(如定时器事件、网络事件、投递的事件、队列连接的信号槽调用)。
QThread::exec(): 进入事件循环(在 run() 方法中调用)。
Worker 对象 + moveToThread + 事件循环模式: 这是现代 Qt 多线程编程的主流模式。对象被移动到新线程后,它的槽函数将在新线程的事件循环中被调用。

6.信号与槽 (Signals & Slots) 的连接类型
Qt 的信号槽机制是线程安全的。
连接类型 (Connection Type) 决定槽函数在哪个线程执行:
Qt::AutoConnection (默认): 如果发送者和接收者在同一线程,行为同 DirectConnection;否则,行为同 QueuedConnection。
Qt::DirectConnection: 槽函数在发送者所在线程中立即执行(就像直接函数调用)。
Qt::QueuedConnection: 槽函数的调用被转换为一个事件,放入接收者所在线程的事件队列。接收者线程的事件循环稍后会从队列中取出并执行该槽函数。这是跨线程通信最安全、最常用的方式!
Qt::BlockingQueuedConnection: 类似 QueuedConnection,但发送者线程会阻塞,直到接收者线程的槽函数执行完毕。慎用!容易死锁。 确保接收者线程能及时处理事件。
Qt::UniqueConnection: 可以与上述类型组合使用 (AutoConnection | UniqueConnection),确保相同的信号和槽之间只有一个连接。

跨线程信号槽参数传递: 参数类型必须是 Qt 元对象系统已知的类型( 使用qRegisterMetaType() 注册自定义类型)。对于 QueuedConnection 和 BlockingQueuedConnection,参数会被复制传递。

二、创建和管理线程

1.使用 QThread 的两种主要模式:
使用 QThread 的两种主要模式:
1.1 子类化 QThread (传统方式):
(1)继承 QThread。
(2)重写 run() 方法,将需要在线程中执行的代码放入其中。
(3)创建子类实例,调用 start()。
局限: run() 是唯一入口,难以处理多个任务或利用事件循环。对象本身(this)仍留在原线程(通常是主线程)。
在QThread子类的构造函数中创建的对象,其线程亲和性是创建该QThread对象的线程(通常是主线程),而不是新线程。这会导致在该对象上使用定时器、信号槽等出现问题。正确的方式是在run()函数中创建这些对象,这样它们的线程亲和性才是新线程。但这样又使得对象创建在run()函数内部,难以从外部管理。线程结束时要清理run()中创建的对象,需要自己管理,容易出错。

1.2 Worker 对象 + moveToThread (推荐方式):
(1)创建一个普通的 QObject 子类(Worker 对象),它包含需要通过槽函数执行的任务。
(2)创建一个 QThread 实例。
(3)创建 Worker 对象实例(此时它在创建者线程,通常是主线程)。
(4)调用 workerObject->moveToThread(workerThread)。关键步骤!
(5)连接 Worker 对象的信号和槽(通常使用 QueuedConnection,但 AutoConnection 在 moveToThread 后通常也能正确处理)。
(6)启动线程 workerThread->start()。这会启动线程的事件循环。
(7)通过信号触发 Worker 对象的槽函数执行任务。任务将在新线程中执行。
(8).请求线程退出:workerThread->quit() 或 workerThread->requestInterruption()(更安全)。
(9)连接 workerThread->finished() 信号到 workerThread->deleteLater() 和 workerObject->deleteLater() 进行自动清理。

//主线程中创建:QThread 实例和worker 对象
Worker* worker = new Worker(); 
QThread* thread = new QThread(this);
worker->moveToThread(thread );
//现在worker属于新线程
thread->start(); // 内部调用exec()启动事件循环
//通过信号槽提交任务
QObject::connect(this, &Controller::startTask, worker, &Worker::doWork);
// 线程结束时自动清理,不然手动在析构函数中delect worker也可以
connect(thread, &QThread::finished, worker, &QObject::deleteLater);
/*因为worker有指定父对象所有这儿不用删除了
connect(thread, &QThread::finished, thread, &QObject::deleteLater); */// 在子线程执行耗时操作    
void Worker::doWork() {............
}      

在这里插入图片描述
关键原则:谁创建谁删除,跨线程对象使用 deleteLater

在这里插入图片描述

新旧方式的对比
在这里插入图片描述

在 Worker 对象中不要做:
创建 QWidget 或其子类(GUI 对象必须在 GUI 线程创建)。
直接操作 GUI(通过信号通知 GUI 线程更新)。
优点: 更符合 Qt 对象模型,可以利用事件循环处理多个任务(定时器、网络等),更灵活,资源管理更清晰。

2.线程池 (QThreadPool 和 QtConcurrent)
QThreadPool: 管理一组可重用的线程。用于执行 QRunnable 任务。
QRunnable: 定义需要执行的任务(重写 run() 方法)。
QThreadPool::globalInstance(): 获取全局线程池实例。
QThreadPool::start(QRunnable *task, int priority = 0): 提交任务到线程池。
优点: 避免频繁创建销毁线程的开销,控制最大并发数。

QtConcurrent 命名空间: 提供高级 API 简化并行计算,底层通常使用 QThreadPool。
run(Function function, …): 在单独线程中运行函数。
map(), mapped(), filtered(), reduce() 等:对容器进行并行操作。
QFuture, QFutureWatcher: 用于监控异步计算的结果和状态。
优点: 代码简洁,易于使用,适合数据并行任务。

三、线程同步与通信

1.互斥锁 QMutex 保护共享数据访问

2.读写锁 (QReadWriteLock, QReadLocker, QWriteLocker) 优化“读多写少”场景。

3.信号量(QSemaphore) 控制对多个相同资源的访问。

4.条件变量
允许线程在特定条件不满足时睡眠等待,并在条件可能改变时被其他线程唤醒。

wait(QMutex *lockedMutex): 原子操作: 释放 lockedMutex 并阻塞等待。被唤醒后,在返回前会重新获取 lockedMutex。

wakeOne(): 唤醒一个等待的线程(任意)。

wakeAll(): 唤醒所有等待的线程。

经典模式: 生产者-消费者。

5.原子操作
对基本数据类型(整数、指针)提供无锁的原子操作(读、写、加减、比较交换等)。

轻量级, 适用于简单的计数器、标志位等场景。不能替代锁保护复杂操作或多变量。
6.跨线程通信的首选:信号与槽
这是 Qt 中最安全、最便捷的跨线程通信机制。利用事件循环传递消息

四、线程安全与最佳实践

1.GUI 线程规则 (黄金法则):
所有用户界面操作(创建、访问、更新 QWidget 及其子类)都必须在主线程(GUI 线程)中进行。

子线程需要更新 UI 时,必须通过信号槽(QueuedConnection)通知主线程进行更新。
2.资源管理
对象树与所有权: Qt 的父子关系管理在跨线程时不适用。父对象和子对象必须在同一线程。

动态对象创建与销毁:

在哪个线程创建对象,该对象通常就“属于”那个线程。

使用 moveToThread() 改变所有权。

安全删除: 使用 obj->deleteLater()。该方法会将删除请求放入对象所在线程的事件队列,由事件循环安全地执行删除操作。这是跨线程删除对象的正确方式! 特别是在连接 QThread::finished() 信号时使用。

3.避免死锁
遵循锁的固定顺序。

最小化临界区(持锁时间)。

谨慎使用嵌套锁。

优先使用 RAII 锁管理 (QMutexLocker 等)。

避免在持锁时等待另一个线程的信号(容易死锁),或使用带超时的等待。
4.退出
请求退出,而非强制终止 (terminate() 非常危险,可能导致资源泄露、状态不一致,应避免使用)。

使用 QThread::requestInterruption() 设置中断请求标志。

在 Worker 对象的耗时操作中定期检查 QThread::isInterruptionRequested(),并在检测到时提前退出。

调用 quit() 或 exit() 请求线程退出事件循环。

使用 wait()(可选,需设置合理超时)确保线程结束。

利用 finished() 信号进行清理 (deleteLater)。
5.异常处理
线程中的异常不会传播到创建该线程的线程(如主线程)。

必须在 run() 或 Worker 对象的槽函数内部捕获并处理所有可能的异常,否则会导致线程崩溃(整个进程通常不会退出,但该线程的工作停止)。
6.性能考量
线程创建销毁有开销,优先考虑线程池 (QThreadPool, QtConcurrent)。

同步原语(锁)有开销,尽量减少竞争(锁粒度、读写锁、原子操作)。

跨线程通信(信号槽 QueuedConnection)有事件投递和复制的开销。

平衡线程数量和任务粒度。

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

相关文章:

  • Linux中使用快捷方式加速SSH访问
  • 通俗范畴论13 鸡与蛋的故事番外篇
  • 2D转换之缩放scale
  • 《P2052 [NOI2011] 道路修建》
  • JavaScript:移动端特效--从触屏事件到本地存储
  • (LeetCode 面试经典 150 题 )3. 无重复字符的最长子串 (哈希表+双指针)
  • 两数之和 https://leetcode.cn/problems/two-sum/description/
  • 基于hugo的静态博客站点部署
  • 苹果公司高ROE分析
  • Druid 连接池使用详解
  • 基于 SpringBoot+Uniapp 易丢丢失物招领微信小程序系统设计与实现
  • BugBug.io 使用全流程(202507)
  • Kubernetes持久卷实战
  • zcbus使用数据抽取相当数据量实况
  • 8. JVM类装载的执行过程
  • hive的索引
  • DBeaver连接MySQL8.0报错Public Key Retrieval is not allowed
  • C语言基础知识--位段
  • UE制作的 AI 交互数字人嵌入到 Vue 开发的信息系统中的方法和步骤
  • 【MaterialDesign】谷歌Material(Google Material Icons) 图标英文 对照一览表
  • AI问答:成为合格产品经理所需能力的综合总结
  • dify工作流1:快速上手ai应用
  • 计算机毕业设计Java停车场管理系统 基于Java的智能停车场管理系统开发 Java语言实现的停车场综合管理平台
  • 网络通信模型对比:OSI与TCP/IP参考模型解析
  • 《Java Web程序设计》实验报告三 使用DIV+CSS制作网站首页
  • ServiceNow Portal前端页面实战讲解
  • [案例八] NX二次开发长圆孔的实现(支持实体)
  • C++中Lambda表达式 [ ] 的写法
  • Redis面试精讲 Day 1:Redis核心特性与应用场景
  • 浅谈 Python 中的 yield——生成器对象与函数调用的区别