【Qt】生产者-消费者模式学习笔记

生产者-消费者模式学习笔记

一、生产者-消费者模式通俗介绍

生产者-消费者模式是一种经典的多线程设计模式,核心作用是解耦数据的产生和处理过程,让两者可以独立运行、协同工作。

核心思想

  • 生产者:负责生成数据(比如采集传感器数据、生成测试数据),生成后将数据放入一个"中间缓冲区"。
  • 消费者:负责从缓冲区中取出数据并处理(比如保存到文件、解析计算)。
  • 缓冲区:作为生产者和消费者之间的"桥梁",通常是一个队列(FIFO),解决两者速度不匹配的问题(比如生产者生成快,消费者处理慢时,数据先存在队列里)。

生活类比

就像餐厅里:

  • 厨师(生产者)做菜,做好后放到出菜台(缓冲区);
  • 服务员(消费者)从出菜台取菜,送到顾客桌上;
  • 出菜台就是缓冲区,即使厨师做快了,菜也不会堆积在厨房,服务员也不用一直等着厨师做完。

二、项目代码架构与设计思路

1. 整体架构

项目采用"生产者-消费者+UI控制"的三层结构,核心组件包括:

  • 数据缓冲区(DataQueue):线程安全的队列,连接生产者和消费者。
  • 生产者(ProducerThread):生成测试数据,推入缓冲区。
  • 消费者(CsvFileSaver):从缓冲区取数据,保存到CSV文件。
  • UI控制器(MainWindow):提供按钮控制生产者/消费者的启动/停止、文件保存等。

2. 核心模块设计思路

(1)数据缓冲区(DataQueue)
  • 核心功能:提供线程安全的"存数据"和"取数据"接口,解决多线程并发访问问题。
  • 关键设计
    • QMutex保证队列操作(存/取)的互斥性,避免同时读写导致数据混乱。
    • QWaitCondition实现"队空时消费者等待,有数据时唤醒"的逻辑,减少无效轮询。
    • 支持批量存/取数据(pushBatch/popBatch),提高效率。
    • 固定最大容量,满了自动删除老数据,避免内存溢出。
(2)生产者(ProducerThread)
  • 核心功能 :循环生成测试数据,通过DataQueuepush接口存入缓冲区。
  • 关键设计
    • 继承QThread,重写run方法实现数据生成循环。
    • m_isRunning标记控制循环启停,通过startProduce/stopProduce接口外部控制。
    • 生成数据逻辑封装在generateTestData(全局函数),与生产者解耦。
(3)消费者(CsvFileSaver)
  • 核心功能:从缓冲区取数据,按规则保存到CSV文件。
  • 关键设计
    • 运行在独立子线程(通过moveToThread实现),避免阻塞UI。
    • QTimer定时(2ms)轮询缓冲区(onPollQueue),批量取数据(popBatch)。
    • 支持动态切换文件名(setNewFileName),切换时自动创建新文件并写入表头。
    • 文件操作加锁(m_fileMutex),保证线程安全。
(4)UI控制器(MainWindow)
  • 核心功能:提供可视化控制界面,协调生产者、消费者和缓冲区的工作。
  • 关键设计
    • 布局按钮控制生产者/消费者的启动/停止、保存开关、文件名设置。
    • 通过信号槽连接UI操作与后台逻辑(如点击"启动生产者"调用ProducerThread::startProduce)。
    • 跨线程调用安全处理(如设置文件名时用QMetaObject::invokeMethod+Qt::QueuedConnection)。

3. 关键接口说明

模块 接口名 功能描述
DataQueue push(item, tag) 单个数据存入队列(线程安全)
DataQueue popBatch(out, size) 批量从队列取数据(队空时阻塞等待)
ProducerThread startProduce() 启动生产者线程,开始生成数据
ProducerThread stopProduce() 停止生产者线程,优雅退出循环
CsvFileSaver start() 启动消费者线程,开始轮询队列
CsvFileSaver setNewFileName(name) 标记切换新文件(下次取数据时生效)
CsvFileSaver startSaving() 开启数据保存(仅控制标记,不影响线程)
MainWindow onConfirmFileName() 处理UI输入,触发文件名切换

三、项目中可能遇到的问题及解决办法

1. 线程安全问题(最核心)

  • 问题:多线程(生产者存数据、消费者取数据)同时操作队列,导致数据错乱或崩溃。
  • 解决
    • QMutex对队列的所有读写操作加锁(DataQueue中所有方法均通过QMutexLocker加锁)。
    • 共享变量(如m_isRunningm_isSaving)通过互斥锁保护,避免读写冲突。

2. 队列空/满时的效率问题

  • 问题:消费者一直轮询空队列,或生产者无限制存入数据导致内存暴涨。
  • 解决
    • 队空时,消费者通过QWaitCondition阻塞等待(DataQueue::pop中的wait),有数据时被唤醒,减少CPU占用。
    • 队列设置最大容量(m_maxCapacity),满时自动删除老数据(pushtakeFirst),控制内存使用。

3. 跨线程通信问题

  • 问题:UI线程(MainWindow)直接调用子线程对象(CsvFileSaver)的方法,导致线程不安全。
  • 解决
    • QMetaObject::invokeMethod+Qt::QueuedConnection实现跨线程安全调用(如MainWindow::onConfirmFileNameClicked中设置文件名)。
    • 子线程对象通过moveToThread移到子线程,避免"对象在主线程,方法在子线程执行"的混乱。

4. 线程停止时的资源释放问题

  • 问题:线程强制停止时,文件未关闭、定时器未停止,导致资源泄露或崩溃。
  • 解决
    • 消费者停止时(CsvFileSaver::stop),先停定时器、关闭文件,再退出线程。
    • 生产者通过m_isRunning标记控制循环退出,避免terminate(强制终止线程)的危险操作。

5. 定时器在子线程中的工作问题

  • 问题:定时器在主线程创建,移到子线程后不工作(定时器依赖线程的事件循环)。
  • 解决
    • 在子线程启动后(QThread::started信号)再启动定时器,并绑定Qt::DirectConnectionCsvFileSaver构造函数中),确保定时器在子线程的事件循环中运行。

四、多线程、QThread、QTimer使用方法与注意事项

1. QThread使用要点

  • 创建子线程的正确方式
    • 推荐:创建QObject子类,通过moveToThread移到子线程(非重写run),用信号槽驱动逻辑(如CsvFileSaver)。
    • 次选:重写run方法实现循环(如ProducerThread),但需注意run中无事件循环,定时器等需手动处理。
  • 线程启停
    • 启动:调用start()(触发run或事件循环)。
    • 停止:用quit()(退出事件循环)+wait()(等待线程结束),避免terminate()(强制终止可能导致资源泄露)。
  • 线程安全
    • 子线程对象的成员变量不可被多线程直接访问,需用互斥锁(QMutex)保护。

2. QTimer使用注意事项

  • 定时器与线程绑定:定时器属于创建它的线程,若对象移到子线程,需在子线程启动后再启动定时器(否则依赖的事件循环不在当前线程)。
  • 连接方式 :定时器的timeout信号与槽函数的连接方式需注意:
    • 若槽函数在同一线程:用Qt::AutoConnection(默认)。
    • 若槽函数在子线程(且定时器在子线程启动):可用Qt::DirectConnection(效率更高)。
  • 定时器精度:间隔越小(如2ms),CPU占用越高,需根据实际需求平衡(项目中用2ms是为了快速响应数据)。

3. 多线程通用注意事项

  • 共享数据必须加锁 :任何被多个线程访问的变量(如队列、状态标记),需用QMutexQReadWriteLock保护,避免竞态条件。
  • 跨线程调用用信号槽或invokeMethod :直接在A线程调用B线程对象的方法是危险的,应通过:
    • 信号槽(自动处理线程切换)。
    • QMetaObject::invokeMethod+Qt::QueuedConnection(适用于需要立即调用的场景)。
  • 避免线程阻塞UI:耗时操作(如文件IO、大量计算)必须放在子线程,UI线程只处理界面更新。
  • 资源释放顺序:子线程停止后,再释放其使用的资源(如文件、网络连接),避免线程还在运行时资源已被释放。

4. 结合项目代码理解 mutable 的实际用途

在 C++ 中,mutable 是一个关键字,其核心作用是允许在 const 成员函数中修改被其修饰的成员变量 。这打破了 "const 成员函数不能修改对象成员" 的默认规则,主要用于那些 "逻辑上不属于对象状态,但需要被修改" 的成员变量。

在你的项目中,mutable 主要用于修饰互斥锁(如 QMutex),例如:

cpp 复制代码
// csvfilesaver.h 中
mutable QMutex m_headerMutex;       // 表头操作锁(线程安全)
mutable QMutex m_runMutex;          // 运行标记锁
mutable QMutex m_saveMutex;         // 保存控制锁
mutable QMutex m_fileNameMutex;     // 文件名变更锁
为什么互斥锁需要 mutable

互斥锁(QMutex)的作用是保证多线程对共享资源的安全访问,其核心操作是 lock()unlock()------ 这两个操作会修改互斥锁自身的状态(比如从 "未锁定" 变为 "锁定")。

而项目中访问这些锁的函数可能是 const 成员函数(例如获取状态的函数)。例如:

cpp 复制代码
// 获取表头字符串(逻辑上是"读取"操作,声明为 const 更合理)
QString CsvFileSaver::getHeaderString() const {
    QMutexLocker locker(&m_headerMutex);  // 这里会调用 m_headerMutex.lock(),修改锁的状态
    return m_headerList.join(",") + "\n";
}
  • 函数 getHeaderString() 是 "读取" 操作,逻辑上不需要修改对象的核心状态(如 m_headerList 的内容),因此声明为 const 是合理的。
  • 但它需要锁定 m_headerMutex 以保证线程安全,而 lock() 操作会修改 m_headerMutex 的状态。

如果 m_headerMutex 没有被 mutable 修饰,编译器会报错(const 函数中不能修改非 mutable 成员)。而 mutable 允许这种修改,因为互斥锁的状态变化属于 "实现细节",不属于对象的 "逻辑状态"(用户关心的是 m_headerList 的值,而不是锁的状态)。

总结 mutable 的核心场景
  1. 线程安全的 const 函数 :当 const 成员函数需要通过互斥锁(QMutex 等)保证线程安全时,锁对象必须用 mutable 修饰,否则无法在 const 函数中执行 lock()/unlock()
  2. 缓存 / 计数等辅助状态 :例如对象中用于缓存计算结果的变量,逻辑上不影响对象的 "常量性",但需要在 const 函数中更新,此时可用 mutable

项目地址https://gitee.com/sun874573943/my-gitee-pro.git

相关推荐
立志成为大牛的小牛2 小时前
数据结构——五十九、冒泡排序(王道408)
数据结构·学习·程序人生·考研·算法
试着2 小时前
【VSCode+AI+测试】连接ai大模型
ide·人工智能·vscode·python·学习·编辑器·ai-test
韩曙亮3 小时前
【思维模型】第一性原理 ② ( 利用 “ 第一性原理 “ 进行创新 : 归零 -> 解构 -> 重构 | 跨学科学习 )
学习·重构·第一性原理·思维模型·解构·归零
秦奈3 小时前
Unity复习学习随笔(五):Unity基础
学习·unity·游戏引擎
我的xiaodoujiao3 小时前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 32--开源电商商城系统项目实战--如何区分登录状态
python·学习·测试工具·pytest
电子小子洋酱3 小时前
Linux显示设备驱动开发 Drm驱动&&Makefile
linux·驱动开发·笔记
电子小子洋酱3 小时前
Linux驱动开发学习笔记(更新中)
linux·笔记·单片机
AI即插即用3 小时前
即插即用系列 | MICCAI EM-Net:融合 Mamba 与频域学习的高效 3D 医学图像分割网络
网络·人工智能·深度学习·神经网络·学习·计算机视觉·视觉检测