无锁消息队列

本文探讨了内存缓存的作用,包括提高效率和解决速度不匹配问题,介绍了内存层次结构和缓存一致性协议MESI,重点讲解了原子性、内存序以及无锁消息队列(如ProducerConsumerQueue和MsgQueue)的实现。作者强调了CAS锁在无锁队列中的应用,以确保并发编程中的线程安全。
摘要由CSDN通过智能技术生成

cache

cache表示的是高速缓冲区

为什么要有cache?   

为了更好的利用局部性原理,减少cpu访问主存的次数,提升效率,以及解决主存和cpu运行速度不匹配的问题。

首先我们先了解我们的内存结构
cpu->寄存器->一级缓存->二级缓存->三级缓存->主存。

离cpu的位置越来越换,获取的速度越来越慢。而cpu获取内存的最小单位为cache line。

cache是由cache line(缓存行)组成的。分为三个区域flag、tag、以及data,其中flag中存的是缓存一致性协议中的值,用来标记是否可见。而tag是用来记录辅助缓存地址定位的。data用来存储。

在cpu中有寄存器、一级缓存、二级缓存、三级缓存,cache是缓存的总和;

补充:缓存命中  通俗的意思就是找到自己需要的内存,优先级一级->二级->三级->主存

缓存一致性协议MESI

为什么需要缓存一致性协议?

实现原子性          

在cup运行的过程中可能在内存中修改、添加、删除等操作(基于写回策略),以往cpu操作完成之后都会将数据同步更新到主存,保证可见性,但这样很影响性能;
优化为主存的数据可以和cpu中的数据不一样,如果是写操作缓存命中的情况下直接写入缓存,然后将数据标记为脏数据,如果没有命中,用LRU(最近最少使用)的策略找到一块内存,如果是脏数据刷到主存中去,不是脏数据从内存中读取数据,然后写,标记为脏数据。读的情况类似。

对于单核来说这样没有问题,对于多核,就会造成缓存不一致的问题,一种就是回到之前的写一次,刷一次主存。还有一种是cup遵循监听发布者模式(总线嗅探机制),将事务穿行化,基于lock指令锁总线,有cup操作时候就在总线上广播,其他cpu也就更新,但是这样也会造成资源的浪费(另一个cup不会用到这个数据,也广播)。如何解决这个问题,就用到了MESI一致性协议,也是基于总线嗅探机制实现了事务的串行化,通过状态降低了总线带宽的压力,有了标记就不用每个都锁总线了,对于一些别的cpu没有的数据,对相关内存lock指令锁住就行。

原子性

原子性就是不是0就是1,是一个状态变成另一个状态,不会有中间状态。

要实现原子性要保证操作的指令不会被打断,底层的自旋锁既可以实现。

内存序

有了原子性我们再关注内存序的问题

为什么有内存序?

因为编译器优化重排和cpu指令重排

内存序规定了什么?

规定了多个线程访问同一个内存地址时的语义、某个线程对内存的更新何时能被其他线程看见,某个线程对内存地址访问附近可以做什么优化。

有了内存序,我们可以通过原子变量的原子操作指导如何对代码经行优化,在并发编程时保证逻辑的正确性

原子操作

队原子变量的操作为原子操作

常用的原子操作有

is_lock_free

store

load

exchange

compare_exchange_weak

compare_exchange_strong

fetch_add

fetch_sub

fetch_or

fetch_xor

我们经常通过compare_exchange、exchange、fetch_add等原语操作实现无锁/无等待消息队列

无锁/无等待消息队列

无锁队列(lock-free)/无等待队列(wait-free)是相关但不完全相同的2个概念

lock-free只保证整个系统整体无论如何向前移动,不能保证每个线程的前进速度(可能出现线程饿死),常使用compare_exchange原语实现,可以有循环,但类似compare_exchange实现的自旋锁不行。
wait-free保证每个线程在有限步骤中向前移动,常使用exchange、fetch_add等原语实现,且不包含可能被其他线程影响的循环。

什么情况只能使用无锁消息队列?

1.信号处理程序,应为信号能随时打断当前线程切换到其他线程,可能导致当前线程的锁没有释放,但是其他线程去申请锁,导致死锁。

2.实时系统

什么时候使用无锁队列?

我们使用队列先用有锁的实现,判断是否要保证系统向前移动,再用无锁队列优化,如果不需要保证向前移动,则比较谁的性能高就用谁。

如果任务执行的时间断,用无锁队列,进行长时间的运算,耗时io则用有锁队列。

消息队列

其包括了MPMC\SPMC\MPSC\SPSC

这里介绍ProducerConsumerQueue和MsgQueue2种

ProducerConsumerQueue中主要由锁,队列,条件变量构成。能用到多线程中。

其的缺点为锁的碰撞比较多,生产者vs生产者、消费者vs消费者、生产者vs消费者都有碰撞

如何优化?

MsgQueue其中对生产者队列和消费者队列进行了分离的操作,且结构中有2个锁,读锁和写锁,

这样就能减少读和写的碰撞,只有当取数据且队列中没有数据的时候,才会进行读和写的碰撞(读写队列的切换)。

队列的实现

队列实现可以基于数组和链表

基于数组的优点:可以快速的随机寻找数据,通常更快,但是不是严格无锁的

缺点:需要预先分配内存,扩容耗时。

基于链表的优点:不用预先分配内存,在首尾添加元素快

缺点:容易导致内存碎片,造成内存的浪费,查找元素的效率低。

混合链表和数组的队列
结合2中结构的优点。

对于链表有侵入式和非侵入式

侵入式表示由外部构建的node插入队列之中,这样队列就不用对node经行内存的管理,由外部管理

非侵入式表示由外部传入数据,内部构建node节点,然后插入队列中去。

同时队列的实现要是有界的,无界队列非常的危险。

因为要并发编程,所以我们用CAS(compare and swap)代替锁来实现无锁队列

CAS锁

例如将索引队列的指针或者索引设置成原子变量,那么就保证了队列的线程安全,例如一个线程创建一个node为input,然后_head.exchange(input,std::memory_order_acq_rel);此操作将头指针指向了你所创建的node,头指针的位置已经改变了,而且是原子的,在别的线程读到的头指针不会相同,然后这个线程后续操作将连表链接上就可以,无需上锁。

  • 35
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值