数据结构与算法--消息队列底层实现

1,196 阅读2分钟

消息队列

step1:顺序队列

​ 消息队列用来解决类似生产者-消费者模型问题,按照实现方式,分为基于链表实现的链式队列基于数组实现的顺序队列

​ 对于链式队列,优点是支持无限大,缺点是性能低;

​ 对于顺序队列,优点是性能高,缺点是数量有限(当队列满时生产者就需要等待)

​ 而消息队列为了追求性能和可控性(链式队列理论无限大因此对于内存不可控),绝大多数都采用顺序队列

step2:循环顺序队列

​ 相比普通顺序队列,循环顺序队列在添加、删除操作时无需进行数据搬移,因此我们进一步对顺序队列优化为循环顺序队列

step3:加锁的循环顺序队列

​ 循环顺序队列虽然性能高,但是高并发下会造成写入覆盖、重复读取等问题,例如添加时:

1. void add(int val) {
2.   if ((tail + 1) % size == head) return;
3.   data[tail] = val;
4.   tail = (tail + 1) % size;
6. }

​ 比如线程1执行到第三行后,线程2开始执行add(),就会导致两个数据同时写入同一个tail下标。对此我们的解决办法就是采用同步原语(临界区、锁、条件变量等),也就是使得并行操作在涉及到内存读写时转为串行

step4:针对锁优化

​ 由于调用同步原语会导致上下文切换(用户态->内核态),且一个线程访问共享数据,其他线程必须排队等待,这样很容易成为并行程序的瓶颈

​ 于是有以下几种处理办法:

多次上锁转为批量一次

​ 把多次上锁操作转换为批量一次,从而减少上锁次数

​ 对于生产者往队列添加数据前,先上锁批量申请连续空闲内存再解锁,这样后续添加就无需加锁(因为这段连续内存线程独享)

​ 对于消费者,先上锁申请连续可读内存再解锁,后续读取操作就可以不用加锁了

​ 缺点是,若生产者 A 申请3~6连续内存,生产者 B 紧跟着申请7~9连续内存,那在3~6没有完全写入前,7~9的数据是无法读取的

CAS操作减少加锁粒度

​ CAS即bool CAS(T * pAddr, T hope, T nNew)

​ 仅当*pAddr==hope*pAddr=nNew,否则表明有其它线程操作,需更新最新hope再CAS,它被认为是最基础的原子性操作

​ CAS是以乐观态度运行的,它总是认为当前线程可完成操作。当多个线程同时使用CAS时最终只有一个会成功,失败的线程不会被挂起,而是允许再次尝试(当然也可以主动放弃)