【C++】手写线程安全队列:mutex + condition_variable 实现生产者消费者模型
前面已经分别介绍过互斥锁和条件变量。单独看这些知识点可能比较零散而线程安全队列正好可以把它们串起来。普通的std::queue并不是线程安全的std::queueint tasks;如果一个线程正在tasks.push(10);另一个线程同时tasks.pop();就可能发生数据竞争。因此我们希望封装一个这样的队列ThreadSafeQueueint queue; queue.push(100); int value; queue.wait_and_pop(value);其中push()可以被生产者线程调用wait_and_pop()可以让消费者在没有数据时自动等待。一、普通 queue 为什么不能直接多线程使用先来看一个简单例子#include queue #include thread std::queueint tasks; void Producer() { for (int i 0; i 1000; i) { tasks.push(i); } } void Consumer() { while (!tasks.empty()) { int value tasks.front(); tasks.pop(); } }如果生产者和消费者同时运行std::thread t1(Producer); std::thread t2(Consumer);它们可能同时访问tasks而std::queue本身不会自动加锁。例如消费者执行if (!tasks.empty())刚判断队列不为空生产者或其他消费者就可能修改队列。更典型的问题是if (!tasks.empty()) { int value tasks.front(); tasks.pop(); }这三步并不是一个不可分割的整体检查队列 ↓ 读取队首 ↓ 删除队首因此必须使用互斥锁保护整个操作过程。最简单的写法std::mutex mutex; void Push(int value) { std::lock_guardstd::mutex lock(mutex); tasks.push(value); }取数据时同样加锁bool Pop(int value) { std::lock_guardstd::mutex lock(mutex); if (tasks.empty()) { return false; } value tasks.front(); tasks.pop(); return true; }这样可以保证同一时刻只有一个线程修改队列。不过还有一个问题队列为空时消费者应该怎么办如果不停调用while (!Pop(value)) { }线程会一直空转占用 CPU。所以还需要condition_variable。二、先实现 push 和 try_pop我们先搭建一个基础线程安全队列#include condition_variable #include mutex #include queue templatetypename T class ThreadSafeQueue { public: void push(const T value) { { std::lock_guardstd::mutex lock(mutex_); queue_.push(value); } condition_.notify_one(); } bool try_pop(T value) { std::lock_guardstd::mutex lock(mutex_); if (queue_.empty()) { return false; } value queue_.front(); queue_.pop(); return true; } private: std::queueT queue_; std::mutex mutex_; std::condition_variable condition_; };先看void push(const T value)内部首先std::lock_guardstd::mutex lock(mutex_); queue_.push(value);保证多个生产者不能同时破坏队列内部结构。加入数据以后condition_.notify_one();通知一个正在等待数据的消费者队列中已经有新数据了可以起来检查了。这里故意把notify_one()放在锁作用域外{ std::lock_guardstd::mutex lock(mutex_); queue_.push(value); } condition_.notify_one();而不是std::lock_guardstd::mutex lock(mutex_); queue_.push(value); condition_.notify_one();后一种通常也能保证正确性但唤醒消费者以后消费者还需要重新获取mutex_。如果生产者此时仍然持有锁消费者被唤醒 ↓ 想获取mutex ↓ 生产者还没有释放 ↓ 消费者继续等待所以通常先完成共享数据修改并释放锁再进行通知会更自然。try_pop()则表示尝试取出一个数据如果当前没有数据就立即返回。bool try_pop(T value) { std::lock_guardstd::mutex lock(mutex_); if (queue_.empty()) { return false; } value queue_.front(); queue_.pop(); return true; }使用int value; if (queue.try_pop(value)) { std::cout value \n; } else { std::cout 队列为空\n; }它不会等待。因此try_pop()比较适合有任务就处理 没任务就去做其他事情而线程池工作线程通常需要的是没有任务就睡眠 有任务再醒来这就需要wait_and_pop()。三、wait_and_pop 为什么需要 unique_lock实现void wait_and_pop(T value) { std::unique_lockstd::mutex lock(mutex_); condition_.wait(lock, [this]() { return !queue_.empty(); }); value queue_.front(); queue_.pop(); }这里最重要的一句是condition_.wait(lock, [this]() { return !queue_.empty(); });它表示只要队列为空就继续等待队列不为空以后才继续向下执行。大致等价于while (queue_.empty()) { condition_.wait(lock); }假设队列为空消费者获得mutex_ ↓ 发现queue_为空 ↓ wait释放mutex_ ↓ 消费者睡眠之后生产者queue.push(100);内部执行获得mutex_ ↓ queue_.push(100) ↓ 释放mutex_ ↓ notify_one()消费者被唤醒重新获得mutex_ ↓ 再次检查queue_.empty() ↓ 发现不为空 ↓ 取出数据这里必须使用std::unique_lockstd::mutex而不是std::lock_guardstd::mutex因为wait()睡眠时需要临时unlock醒来以后还需要lockunique_lock支持这种灵活控制。因此std::unique_lockstd::mutex lock(mutex_); condition_.wait(lock, [this]() { return !queue_.empty(); });是线程安全队列中非常经典的一种写法。四、完整线程安全队列实现把几个接口组合起来#include condition_variable #include mutex #include queue templatetypename T class ThreadSafeQueue { public: ThreadSafeQueue() default; void push(const T value) { { std::lock_guardstd::mutex lock(mutex_); queue_.push(value); } condition_.notify_one(); } void push(T value) { { std::lock_guardstd::mutex lock(mutex_); queue_.push(std::move(value)); } condition_.notify_one(); } bool try_pop(T value) { std::lock_guardstd::mutex lock(mutex_); if (queue_.empty()) { return false; } value std::move(queue_.front()); queue_.pop(); return true; } void wait_and_pop(T value) { std::unique_lockstd::mutex lock(mutex_); condition_.wait(lock, [this]() { return !queue_.empty(); }); value std::move(queue_.front()); queue_.pop(); } bool empty() const { std::lock_guardstd::mutex lock(mutex_); return queue_.empty(); } size_t size() const { std::lock_guardstd::mutex lock(mutex_); return queue_.size(); } private: std::queueT queue_; mutable std::mutex mutex_; std::condition_variable condition_; };这里同时提供了两个push()void push(const T value); void push(T value);第一个处理左值int value 100; queue.push(value);第二个支持移动queue.push(100);或者std::string str hello; queue.push(std::move(str));取数据时也使用value std::move(queue_.front());避免某些较大对象发生不必要的复制。另外bool empty() const是const成员函数但里面需要锁住mutex_所以互斥锁声明为mutable std::mutex mutex_;mutable表示即使当前对象是const这个成员仍然允许修改。而加锁和解锁本身会修改 mutex 的内部状态所以需要mutable。五、生产者消费者完整示例下面创建两个生产者和两个消费者。#include iostream #include thread ThreadSafeQueueint queue; void Producer(int start) { for (int i 0; i 5; i) { int value start i; queue.push(value); std::cout 生产 value \n; } } void Consumer() { for (int i 0; i 5; i) { int value; queue.wait_and_pop(value); std::cout 消费 value \n; } } int main() { std::thread producer1(Producer, 100); std::thread producer2(Producer, 200); std::thread consumer1(Consumer); std::thread consumer2(Consumer); producer1.join(); producer2.join(); consumer1.join(); consumer2.join(); return 0; }生产者不断queue.push(value);消费者不断queue.wait_and_pop(value);如果队列中存在数据消费者直接取出如果队列为空消费者进入wait ↓ 释放mutex ↓ 进入睡眠生产者加入任务push数据 ↓ notify_one ↓ 消费者被唤醒这就是最基本的生产者—消费者模型。整个结构其实已经非常接近线程池中的任务队列外部线程 ↓ 提交任务 ↓ ThreadSafeQueue ↓ condition_variable通知 ↓ Worker线程醒来 ↓ wait_and_pop取任务 ↓ 执行任务如果把ThreadSafeQueueint改成ThreadSafeQueuestd::functionvoid()队列里面保存的就不再是数字而是一个个真正可以执行的任务ThreadSafeQueuestd::functionvoid() tasks;提交任务tasks.push([]() { std::cout 执行任务\n; });工作线程std::functionvoid() task; tasks.wait_and_pop(task); task();这样就已经搭出了一个简化线程池最核心的结构任务 ↓ 线程安全队列 ↓ condition_variable ↓ Worker ↓ 执行task()这一篇最需要掌握的是std::queue本身不保证线程安全 mutex负责保护队列内部数据 try_pop没有数据时立即返回 wait_and_pop没有数据时让线程睡眠 condition_variable负责通知等待线程 wait需要unique_lock因为等待期间必须释放mutex push完成数据修改后再notify_one 线程安全队列是生产者消费者模型和线程池的重要基础。0voice · GitHub

相关新闻

社会心理学视角下的祥和假象与危机预警机制

社会心理学视角下的祥和假象与危机预警机制

1. 标题背后的隐喻与现实映射"一片祥和的底下原来是暴风雨"这个充满诗意的标题,实际上揭示了当代社会普遍存在的一种现象——表面平静下暗藏危机。这种现象在心理学上被称为"平静假象效应",指的是人们倾向于展示和谐表象而隐藏真实矛…

2026/10/2 15:22:44 阅读更多 →
【C++】异步任务结果传递:promise、future 与 packaged_task

【C++】异步任务结果传递:promise、future 与 packaged_task

前面介绍了 condition_variable,解决的是“线程没有任务时如何等待,有任务时如何被唤醒”的问题。接下来还有一个很常见的问题: 子线程执行了一个任务以后,任务的返回值怎么交给主线程? 例如: int Add(int…

2026/9/24 20:52:40 阅读更多 →
智能问数POC验证方法论:八个必测场景,告别“demo好看上线翻车“

智能问数POC验证方法论:八个必测场景,告别“demo好看上线翻车“

摘要:传统智能问数/ChatBI的POC做法——准备50个"XX指标是多少"类问题统计准确率——已完全失效。本文提出面向新一代智能问数的POC验证方法论,包括八个必测场景(复杂归因、多轮追问、完整闭环、追问解释、纠错重测、多类型分析、数…

2026/9/29 23:46:36 阅读更多 →

最新新闻

AssetBundle热更新安全排查:从CDN清单到本地缓存的全链路校验指南

AssetBundle热更新安全排查:从CDN清单到本地缓存的全链路校验指南

1. 项目概述:一次热更新安全隐患排查的完整复盘 做 Unity 客户端开发的朋友应该都有体会,AssetBundle 热更新方案上线容易,但真正让它长期稳定跑起来,靠的是细节。尤其是当你的游戏量级上来、CDN 节点分叉、本地缓存策略多样化之后…

2026/10/2 22:53:09 阅读更多 →
Jev浏览器Agent实测:本地部署AI模型驱动浏览器自动化全攻略

Jev浏览器Agent实测:本地部署AI模型驱动浏览器自动化全攻略

最近GitHub上有个叫Jev的浏览器Agent插件火了,21k star,把AI模型和浏览器自动化结合到一起,用自然语言就能驱动浏览器干活。我做了一轮完整的部署和使用测试,从模型选型、本地部署到插件配置、实际跑任务,把整个链路都…

2026/10/2 22:53:09 阅读更多 →
从零搭建AI工程体系:架构设计、核心模块与实操落地指南

从零搭建AI工程体系:架构设计、核心模块与实操落地指南

1. 从零搭建AI工程体系,为什么我劝你别急着调包"ai-engineering-from-scratch"这个标题,第一次看到的时候我愣了一下。市面上讲AI的教程铺天盖地,但绝大多数都是教你pip install一个库,然后调几个API,跑通一…

2026/10/2 22:53:09 阅读更多 →
DeepSeek Harness客户端详解:Token管理与多模型接入实战

DeepSeek Harness客户端详解:Token管理与多模型接入实战

DeepSeek Harness 客户端开放下载,消息一出,不少做 AI 应用开发的朋友都在群里聊这件事。如果你平时经常调 DeepSeek 的 API,或者需要在本地同时管理多个主流大模型的对话与调用,这个客户端确实值得花几分钟试一下。它把模型接入、…

2026/10/2 22:53:09 阅读更多 →
职工考勤管理系统:从数据库设计到状态判定完整实战

职工考勤管理系统:从数据库设计到状态判定完整实战

简介:数据库课程设计——职工考勤管理信息系统完整设计文档,面向计算机相关专业学生及需要完成数据库课程设计的人员。文档以企业考勤管理为背景,系统阐述从需求分析、概念结构设计到逻辑结构设计、物理结构设计与数据库实施的完整流程&#…

2026/10/2 22:53:09 阅读更多 →
互联网商业医疗保险直付平台:从理赔垫付到秒级结算的落地拆解

互联网商业医疗保险直付平台:从理赔垫付到秒级结算的落地拆解

简介:这份PDF文献面向医疗信息化从业者、医院信息中心技术人员及医疗保障研究者,聚焦互联网商业医疗保险直付平台的解决方案。内容系统梳理了商保的概况与现状、传统理赔流程的痛点,并重点论述平台设计原则,包括数据安全、实时性、…

2026/10/2 22:52:08 阅读更多 →

日新闻

从零搭建AI工程化:模型之外的完整闭环

从零搭建AI工程化:模型之外的完整闭环

先搞清楚一件事:从零开始做 AI 工程化,难的从来不是调模型、写提示词,而是把一套原型 Demo 变成长得像是“正经系统”的东西。你手里可能已经有了能跑通的代码,也可能刚读完一些概念,但真到了要把它变成可维护、可观测…

2026/10/2 0:00:20 阅读更多 →
大模型训练显存估计与混合精度训练实战指南

大模型训练显存估计与混合精度训练实战指南

1. 大模型训练显存估计与混合精度训练详解显存不够用,几乎是每个做大模型训练的人都会撞上的第一堵墙。你可能也经历过:模型代码写完了,数据管道跑通了,满心欢喜地按下训练启动脚本,结果几秒钟后终端弹出一行红字——C…

2026/10/2 0:00:20 阅读更多 →
小样本学习数据集选型指南:27个真正可用的高质量数据集

小样本学习数据集选型指南:27个真正可用的高质量数据集

1. 小样本学习的“弹药库”:为什么你总在找数据集,却总找不到真正能用的? 小样本、数据集——这两个词最近半年在我处理的200多个AI项目咨询里,出现频率排进前三。不是模型调不好,不是代码写不对,而是卡在…

2026/10/2 0:00:20 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/10/1 19:40:48 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/10/1 19:41:40 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/10/1 20:05:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 10:36:31 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 5:26:06 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/2 6:09:11 阅读更多 →