无锁并发——环形队列

Posted by 皮皮潘 on 08-29,2023

这里先简单地记录一下 DPDK 中无锁环形队列的大致实现,之后有空会补上更详细的示意图与代码。

无锁环形队列的核心在于通过 CAS 实现无锁地预获取资源空间,从而保证之后的操作之间的并发隔离性,进而实现并发地处理各自的资源空间而无需加锁。

在具体实现中,生产者和消费者都各自拥有一个 head 指针,代表可插入或者可消费的第一个元素,其中 consume_head 代表当前可消费的第一个元素,prod_head 代表当前可被插入元素的第一个空位,考虑到并发情况,在具体消费和生产时都会通过 CAS 去更新对应的全局的 head 指针为 head+n(一般为1,批量情况下为 n),如果更新成功,则会记录旧全局 head 到本地,也代表拥有了[旧 head , head + n) 元素的使用权用于生产或者消费。

由于允许多个消费者或者多个生产者并发执行,所以又引入了 tail 指针代表最旧的且正在执行消费或者生产的元素,其中 consume_tail 代表当前最旧的正在执行消费的元素,prod_tail 代表当前最旧的正在执行生产的元素,更具体的: [consume_tail, consume_head) 之间的元素代表了正在消费但是没有消费结束的元素,而[prod_tail, prod_head) 之间的元素代表了正在插入但是没有插入结束的元素,而[consume_head, prod_tail)之间的元素则是真正的待消费的元素,剩余的元素[0, consume_tail) + [prod_tail, n) 则是空位元素,可以被生产者插入。

在准备消费或者生产某个元素之前会先 CAS 对应的 head,来获得 [head, head+n) 之间的元素的使用权,此处注意在 CAS 对应的 head 之前需要先判断待消费的元素是否为 0 或者剩余空间是否为 0 来决定是否足够生产或者消费,从而保证消费者和生产者之间的并发一致性。在完成了某个元素的消费或者生产之后,则更新对应的 tail 指针,此时采用了类似 CAS 但是更高效的方式去替换,也即比较局部的旧 head 是否和全局的 tail 一致,如果一致就更新全局的 tail 为 head + n(这时候由于并发消费者和并发生产者,全局的tail 和全局的 head 可能还不一样),反之则代表还有更旧的消费或者生产操作还未结束,等待自身变为最旧的再做替换即可。