【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. 关键接口说明

模块接口名功能描述
DataQueuepush(item, tag)单个数据存入队列(线程安全)
DataQueuepopBatch(out, size)批量从队列取数据(队空时阻塞等待)
ProducerThreadstartProduce()启动生产者线程,开始生成数据
ProducerThreadstopProduce()停止生产者线程,优雅退出循环
CsvFileSaverstart()启动消费者线程,开始轮询队列
CsvFileSaversetNewFileName(name)标记切换新文件(下次取数据时生效)
CsvFileSaverstartSaving()开启数据保存(仅控制标记,不影响线程)
MainWindowonConfirmFileName()处理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),例如:

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

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

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

// 获取表头字符串(逻辑上是“读取”操作,声明为 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值