无锁队列设计

news2026/3/17 4:52:01
无锁队列设计文章目录无锁队列设计1. 为什么需要无锁队列2. 无锁编程基本概念2.1 阻塞Blocking、无锁Lock-Free与无等待Wait-Free2.2 无锁编程的挑战3. 无锁队列的分类4. SPSC环形缓冲区实现4.1 基本设计要点4.2 ringBuffer实现带详细注释4.3 关键点解析5. MPSC链表实现5.1 设计思路5.2 非侵入式实现5.3 侵入式实现6. 无锁队列设计中的考虑6.1 内存序的正确选择6.2 内存管理6.4 性能调优7. 性能对比与适用场景8. 总结在多线程编程中队列是一种常用的数据结构用于在生产者和消费者之间传递数据。然而传统的基于锁的队列在高并发场景下会引入性能瓶颈线程阻塞、上下文切换、缓存行失效等。无锁队列Lock-Free Queue通过原子操作和精心设计的数据结构避免了锁的开销在某些场景下能大幅提升性能。本文将深入探讨无锁队列的设计思想、常见实现及注意事项。1. 为什么需要无锁队列多线程环境下的锁竞争会带来以下问题线程切换开销当锁被持有时其他线程必须等待操作系统可能进行上下文切换耗时可达微秒级。缓存损坏Cache Pollution锁的争用会导致内核调度器干预使CPU缓存失效后续重新加载数据需要时间。任务执行时间不确定在硬实时系统或信号处理程序中无法容忍阻塞等待。优先级反转低优先级线程持有锁高优先级线程等待可能导致系统不可预测。无锁队列允许多个线程并发访问而不使用互斥锁通过原子操作如CAS、Exchange来保证数据一致性从而避免上述问题。2. 无锁编程基本概念2.1 阻塞Blocking、无锁Lock-Free与无等待Wait-Free阻塞Blocking使用互斥锁、信号量等同步机制线程在无法获取资源时会进入休眠由操作系统调度唤醒。无锁Lock-Free整个系统的整体进度有保证即任意线程被暂停其他线程仍能继续完成操作。通常通过原子操作实现但单个线程可能面临重试如CAS失败。无等待Wait-Free在无锁的基础上更进一步保证每个线程的操作在有限步内完成不会因为其他线程的干扰而无限重试。实现难度极高。2.2 无锁编程的挑战内存管理在多生产者多消费者场景下如何安全释放节点内存常用技术有Hazard Pointer、RCU读-复制-更新、引用计数等。内存顺序Memory Ordering原子操作需要配合恰当的内存顺序如memory_order_acquire/release/relaxed以保证跨线程的可见性和有序性。3. 无锁队列的分类根据生产者和消费者的数量无锁队列可分为类型全称适用场景SPSCSingle Producer Single Consumer单一生产者和单一消费者实现最简单性能最高SPMCSingle Producer Multiple Consumers单一生产者多个消费者较少见MPSCMultiple Producers Single Consumer多个生产者单一消费者如日志收集、任务分发MPMCMultiple Producers Multiple Consumers多个生产者多个消费者通用但实现复杂本文将重点讲解SPSC环形缓冲区和MPSC链表队列的实现。4. SPSC环形缓冲区实现环形缓冲区Ring Buffer基于固定大小的数组通过头尾索引实现FIFO。SPSC场景下生产者和消费者分别操作tail_和head_索引通过原子操作保证线程安全。4.1 基本设计要点数组容量设为2的幂次以便将取模运算优化为位与运算next (curr 1) (cap - 1)。存储任意类型使用std::aligned_storage预留内存通过placement new构造/析构对象支持非POD类型。伪共享False Sharing将head_、tail_和缓冲区数据放在不同的缓存行通常64字节避免频繁同步导致性能下降。内存序利用memory_order_acquire/release保证写操作在消费者读之前可见且读操作能看到最新的写入。4.2 ringBuffer实现带详细注释#pragmaonce#includeatomic#includeutility// std::forward#includecstddef// std::size_t// SPSC 环形缓冲区容量必须为2的幂templatetypenameT,std::size_t CapacityclassringBuffer{public:static_assert(Capacity((Capacity(Capacity-1))0),Capacity must be a power of 2);ringBuffer():head_(0),tail_(0){}~ringBuffer(){// 析构时若队列中还有未消费的元素需要手动调用析构函数std::size_t headhead_.load(std::memory_order_relaxed);std::size_t tailtail_.load(std::memory_order_relaxed);while(head!tail){reinterpret_castT*(buffer_[head])-~T();head(head1)(Capacity-1);}}// 万能引用支持左值和右值templatetypenameUboolPush(Uvalue){std::size_t tailtail_.load(std::memory_order_relaxed);std::size_t next_tail(tail1)(Capacity-1);// 检查队列是否满下一个tail位置等于head表示无空位if(next_tailhead_.load(std::memory_order_acquire)){returnfalse;// 队列满}// 在缓冲区尾部构造对象placement newnew(buffer_[tail])T(std::forwardU(value));// 更新tail使用release语义确保之前的构造对其他线程可见tail_.store(next_tail,std::memory_order_release);returntrue;}boolPop(Tvalue){std::size_t headhead_.load(std::memory_order_relaxed);if(headtail_.load(std::memory_order_acquire)){returnfalse;// 队列空}// 移动取出元素使用move优化避免拷贝valuestd::move(*reinterpret_castT*(buffer_[head]));// 显式析构对象reinterpret_castT*(buffer_[head])-~T();// 更新head使用release语义保证之前的析构和移动操作对其他线程可见head_.store((head1)(Capacity-1),std::memory_order_release);returntrue;}// 返回当前队列中元素个数注意非原子快照仅用于调试std::size_tSize()const{conststd::size_t headhead_.load(std::memory_order_relaxed);conststd::size_t tailtail_.load(std::memory_order_relaxed);returntailhead?tail-head:Capacity-(head-tail);}private:// 将head_和tail_各自对齐到64字节避免与buffer_共享缓存行alignas(64)std::atomicstd::size_thead_;alignas(64)std::atomicstd::size_ttail_;// 存储元素的原始内存保证对齐alignas(64)std::aligned_storage_tsizeof(T),alignof(T)buffer_[Capacity];};4.3 关键点解析容量为2的幂(tail 1) (Capacity - 1)等价于(tail 1) % Capacity但位运算更快。内存对齐alignas(64)强制将变量放在64字节边界避免与相邻变量共享缓存行减少伪共享。内存序选择tail_.load(std::memory_order_relaxed)生产者读自己的tail不需要与其他线程同步。head_.load(std::memory_order_acquire)需要看到消费者最新的head值保证判满的准确性。tail_.store(..., release)使之前对缓冲区的写入在消费者看到新tail时可见。对象生命周期使用placement new构造手动析构确保非POD类型正确释放资源。右值支持Push接受万能引用完美转发避免不必要的拷贝。5. MPSC链表实现当有多个生产者但只有一个消费者时可以采用基于链表的MPSC队列。链表结构可以动态增长不受固定容量限制适合任务数不确定的场景。5.1 设计思路head指针始终指向最新插入的节点多个生产者通过原子exchange竞争插入。tail指针指向最早插入的节点即队列头只有消费者会移动它。入队时生产者创建一个新节点将其next置为nullptr然后将head原子交换为新节点同时得到旧head再将旧head的next指向新节点。这样就形成了一个从旧head指向新head的链表实际上是逆序的但消费者从tail正向遍历。出队时消费者检查tail-next若不为空则移动tail取出数据并删除原tail节点。5.2 非侵入式实现非侵入式队列中节点包含数据指针和next指针内存由队列管理。#ifndefMPSC_QUEUE_NON_INTRUSIVE_H#defineMPSC_QUEUE_NON_INTRUSIVE_H#includeatomic#includeutilitytemplatetypenameTclassMPSCQueueNonIntrusive{public:MPSCQueueNonIntrusive():_head(newNode()),_tail(_head.load(std::memory_order_relaxed)){Node*front_head.load(std::memory_order_relaxed);front-Next.store(nullptr,std::memory_order_relaxed);}~MPSCQueueNonIntrusive(){T*output;while(Dequeue(output))deleteoutput;// 释放数据对象Node*front_head.load(std::memory_order_relaxed);deletefront;// 释放最后一个节点可能是dummy}// 多生产者入队wait-freevoidEnqueue(T*input){Node*nodenewNode(input);Node*prevHead_head.exchange(node,std::memory_order_acq_rel);prevHead-Next.store(node,std::memory_order_release);}// 单消费者出队boolDequeue(T*result){Node*tail_tail.load(std::memory_order_relaxed);Node*nexttail-Next.load(std::memory_order_acquire);if(!next)returnfalse;// 队列空resultnext-Data;_tail.store(next,std::memory_order_release);deletetail;// 删除原tail节点returntrue;}private:structNode{Node()default;explicitNode(T*data):Data(data){Next.store(nullptr,std::memory_order_relaxed);}T*Data;std::atomicNode*Next;};std::atomicNode*_head;std::atomicNode*_tail;// 禁止拷贝MPSCQueueNonIntrusive(constMPSCQueueNonIntrusive)delete;MPSCQueueNonIntrusiveoperator(constMPSCQueueNonIntrusive)delete;};#endif说明构造函数中创建了一个dummy节点使head和tail初始指向它避免空指针判断。Enqueue_head.exchange(node, acq_rel)原子地将_head更新为新节点并返回旧head。然后设置旧head的Next指向新节点。这一步保证了多生产者并发时的正确链接。Dequeue消费者从tail开始如果tail-Next存在则取出数据更新tail并删除原tail节点。内存序exchange使用acq_rel读取旧head需要acquire写入新head需要release同时保证对旧head的后续操作可见。Next.store使用release确保节点完全构造后再让消费者看到。Next.load使用acquire保证看到生产者设置的Next。5.3 侵入式实现侵入式队列要求节点类型T内部包含一个std::atomicT*成员作为next指针队列操作直接利用该成员无需额外分配节点对象节省内存减少缓存缺失。#ifndefMPSC_QUEUE_INTRUSIVE_H#defineMPSC_QUEUE_INTRUSIVE_H#includeatomic#includetype_traits#includenewtemplatetypenameT,std::atomicT*T::*IntrusiveLinkclassMPSCQueueIntrusive{public:MPSCQueueIntrusive():_dummyPtr(reinterpret_castT*(std::addressof(_dummy))),_head(_dummyPtr),_tail(_dummyPtr){// 只初始化dummy节点的IntrusiveLink成员因为T可能不可默认构造std::atomicT**dummyNextnew((_dummyPtr-*IntrusiveLink))std::atomicT*();dummyNext-store(nullptr,std::memory_order_relaxed);}~MPSCQueueIntrusive(){T*output;while(Dequeue(output)){deleteoutput;// 注意这里删除的是数据对象本身其内存可能由外部管理需谨慎}}// 入队voidEnqueue(T*input){(input-*IntrusiveLink).store(nullptr,std::memory_order_release);T*prevHead_head.exchange(input,std::memory_order_acq_rel);(prevHead-*IntrusiveLink).store(input,std::memory_order_release);}// 出队boolDequeue(T*result){T*tail_tail.load(std::memory_order_relaxed);T*next(tail-*IntrusiveLink).load(std::memory_order_acquire);// 如果tail是dummy节点需要特殊处理跳过dummyif(tail_dummyPtr){if(!next)returnfalse;// 队列空_tail.store(next,std::memory_order_release);tailnext;next(next-*IntrusiveLink).load(std::memory_order_acquire);}if(next){_tail.store(next,std::memory_order_release);resulttail;returntrue;}// 此时tail可能是最后一个节点需要检查是否有新节点刚入队T*head_head.load(std::memory_order_acquire);if(tail!head)// 如果head已经更新但tail-Next尚未设置可能刚执行exchange但未设置Nextreturnfalse;// 让消费者重试此处简单返回false实际可自旋或依赖外部重试// 队列为空但需要将dummy重新入队以便下次使用防止tail超过headEnqueue(_dummyPtr);next(tail-*IntrusiveLink).load(std::memory_order_acquire);if(next){_tail.store(next,std::memory_order_release);resulttail;returntrue;}returnfalse;}private:std::aligned_storage_tsizeof(T),alignof(T)_dummy;T*_dummyPtr;std::atomicT*_head;std::atomicT*_tail;MPSCQueueIntrusive(constMPSCQueueIntrusive)delete;MPSCQueueIntrusiveoperator(constMPSCQueueIntrusive)delete;};#endif要点_dummy是一个aligned_storage只用来占位不构造T对象仅初始化其IntrusiveLink成员。出队逻辑复杂因为存在dummy节点且可能出现刚入队但next尚未设置的情况需要特殊处理。使用std::conditional_t可以定义一个统一的MPSCQueue别名根据是否提供IntrusiveLink自动选择版本。6. 无锁队列设计中的考虑6.1 内存序的正确选择C11提供了六种内存顺序理解它们对无锁编程至关重要memory_order_relaxed仅保证原子性无顺序约束。memory_order_acquire防止之后的内存读写被重排到该操作之前。memory_order_release防止之前的内存读写被重排到该操作之后。memory_order_acq_rel读-修改-写操作同时拥有acquire和release语义。memory_order_seq_cst顺序一致性最严格但也最慢。在SPSC中我们用acquire读对方的索引用release写自己的索引保证了队列操作的顺序。在MPSC中exchange使用acq_rel确保获取旧head的完整可见性。6.2 内存管理链式队列需要动态分配节点。频繁的new/delete可能成为性能瓶颈。优化手段包括内存池预先分配一大块内存节点复用。侵入式设计数据对象自身携带next指针减少一次分配。Hazard Pointer安全回收内存避免悬挂指针。6.4 性能调优避免伪共享将频繁修改的变量如head、tail分散到不同缓存行。批量操作一次push/pop多个元素分摊原子操作开销。选择合适的容量环形缓冲区过大浪费内存过小增加等待。7. 性能对比与适用场景类型优点缺点适用场景SPSC环形缓冲区极高性能无锁且无等待容量固定不适用于动态增长音频处理、实时数据流、单生产者单消费者管道MPSC链表支持多生产者动态大小节点分配开销出队可能稍慢日志聚合、任务队列、事件分发实际选择时需根据生产者/消费者数量、对延迟和吞吐的要求、内存限制等因素权衡。8. 总结无锁队列是高性能并发编程的重要组件。本文从基础概念出发详细剖析了SPSC环形缓冲区和MPSC链表队列的实现并讨论了内存序、内存管理等核心挑战。理解这些设计思想后读者可以根据实际需求实现或选用合适的无锁队列。https://github.com/0voice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2414974.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…