news 2026/5/11 14:36:18

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【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 中mutableQMutex m_headerMutex;// 表头操作锁(线程安全)mutableQMutex m_runMutex;// 运行标记锁mutableQMutex m_saveMutex;// 保存控制锁mutableQMutex m_fileNameMutex;// 文件名变更锁
为什么互斥锁需要mutable

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

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

// 获取表头字符串(逻辑上是“读取”操作,声明为 const 更合理)QStringCsvFileSaver::getHeaderString()const{QMutexLockerlocker(&m_headerMutex);// 这里会调用 m_headerMutex.lock(),修改锁的状态returnm_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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/10 6:27:39

音效神器,绝了

今天给大家带来一款可以让电脑WIN10/WIN11简单安装杜比音效 软件,这样电脑看电影、听歌,都有一个专业的音效体验感。 杜比音效 win10/win11适用 文件下载解压后,双击杜比安装器1.3.exe这个文件,选择安装驱动。 在驱动列表里面选择…

作者头像 李华
网站建设 2026/5/9 2:56:58

命令行上传和更新本地项目到github的步骤

一、上传项目0)如果项目本身有git残留,需要先移除旧的origin:git remote remove origin1)首次上传:git remote add origin https://github.com/用户名/项目名称2)然后推送:git push -u origin m…

作者头像 李华
网站建设 2026/5/10 20:11:50

30、Ubuntu不同版本及Wine使用全解析

Ubuntu不同版本及Wine使用全解析 1. Ubuntu Netbook Edition Ubuntu Netbook Edition(在Ubuntu 10.04之前的版本中称为Netbook Remix)是专门为上网本电脑重新设计的版本。上网本通常处理能力较弱、屏幕较小,如果你觉得当前操作系统运行缓慢且响应不佳,那么这个版本可能很适…

作者头像 李华
网站建设 2026/5/10 20:11:53

31、Ubuntu 使用指南与技巧全解析

Ubuntu 使用指南与技巧全解析 1. 访问模拟的 Windows C 盘 在 Ubuntu 中,若要使用终端切换到模拟的 c:\Program Files 文件夹,可输入以下命令: cd ~/.wine/dosdevices/c:/"Program Files"需注意,对于包含非字母数字字符的名称,要使用引号括起来。另外,也…

作者头像 李华
网站建设 2026/5/10 20:11:52

Helm:K8s应用部署的终极利器

一、Helm概述 helm通过打包的方式,支持发布的版本管理和控制,很大程度上简化了Kubernetes应用的部署和管理。 Helm本质就是让k8s的应用管理(Deployment、Service等)可配置,能动态生成。通过动态生成K8S资源清单文&am…

作者头像 李华
网站建设 2026/5/10 13:11:58

黑马点评前125节课遇到的问题及解决方案(在看网课过程中会有很多老师运行成功但我们失败并且老师还不没有讲到的情况,本文致力于解决这个问题,记录了本人在做这个项目的时候遇到的所有问题)

写在前面的话 在看网课过程中会有很多老师运行成功但我们失败并且老师还不没有讲到的情况,本文致力于解决这个问题,记录了本人在做这个项目的时候遇到的所有问题,希望可以帮助到大家。、 可以直接在根据目录或者导航栏找到自己的遇到问题的…

作者头像 李华