ZeroMQ无锁队列分析

ZeroMQ无锁队列分析

1. 概述

ZeroMQ使用了一个无锁队列,用于线程间的高性能数据交换。这个无锁队列由两个对象组成:

  • yqueue_t: 一个普通的队列,实现内存块链表,以及内存块的回收和重复利用。
  • ypipe_t: 基于yqueue_t实现的无锁管道队列,实现一读一写的无锁操作。

2. yqueue_t类

yqueue_t类实现了一个普通的队列(多线程不安全),但为了提高性能,使用了内存块链的方式,每个内存块可支持多个数据元素节点。
yqueue链表结构

  • begin_chunk: 队列第一个内存块。
  • end_chunk: 队列最后一个内存块。
  • begin_pos: 队列中第一个元素在第一个内存块中的位置。
  • end_pos: 对垒中最后一个元素在最后一个内存块的位置。

同时为了减少内存的分配、释放操作,yqueue_t中包含了一个spare_chunk,存放一个空闲的内存块。该spare_chunk的首节点指针使用atomic变量存放,实现spare_chunk中存取的线程安全。

2.1 push操作

push操作实现向队列中增加一个空元素,此后可使用back方法获取新增元素的引用,然后将数据填入。简要操作过程:

// end pos后移, 如果达到当前chunk末尾,就新增一个chunk.
// 获取新chunk,可以从spare_chunk获取,也可能是重新内存分配。
if ( ++end_pos = N )     
    create new_chunk;  
    end_chunk = move_next;
    end_pos = 0;
endif

2.2 pop操作

pop操作实现队列前端的后移,最前元素被移出队列。

if ( ++begin_pos = N )     
    delete begin_chunk;   // begin chunk取空后,被删除。
    begin_chunk move next;
    begin_pos = 0;
endif

2.3 问题

  • 根据ZeroMQ的源代码,在pop一个元素后,并没有对该元素执行析构操作,也没有在释放chunk时 取去析构每个元素,因此,该队列不适用于需要显式析构的元素对象(如数据对象中包含需要释放 的指针成员)。

  • pop和push操作的是不同的位置变量,两个操作本身不存在多线程冲突。但由于没有空队列的检测机制,对空队列进行pop操作会导致队列异常。而 空队列检查需要同时访问写入位置和读取位置,因此如果pop/posh放弃空队列检查,目的在于确保pop/push在一读一写并发时的线程安全。而空队 列时的操作检测,则交给了ypipe_t类实现。

3. ypipe_t类

ypipe_t类在yqueue_t的基础上,主要在读数据前,增加了空队列的检查机制。同时为了实现性能最大化,使用了基于atomic指针和一种预取机制的方案。

在ypipt_t类中,除了用于存储数据的yqueue_t对象外(q), 增加了如下几个成员:

  • c: atomic原子量指针,指向下一次的写入位置,也是最后一个可读元素的后一个位置,作为元素写入和读取域的边界,是空队列检查在一读一写并发时的线程安全的关键。
  • *w: 指向队列中最近写入且尚未flush提交的元素。ypipe_t
  • r: 指向第一个没有预取的元素,该指针仅由读取操作访问。
  • f: 指向将被flush的元素。

3.1 初始化状态图解

ypipe初始化状态

  • 链表为空,front地址指向第一个元素,即下次的写入位置
  • 上述r/w/f/c同时指向front,即下次写入位置。

3.2 写入元素

写入过程逻辑(不考虑批量写入的flush机会,假设写入一个元素后立即flush):

q.back() = value;    // 向back位置写入数据
q.push();            // back位置后移
f = q.back();        // f指向新的back位置

ypipe数据写入后状态

3.3 队列可读检查(check_read)和数据读取

在读出数据前,首先执行了空队列检查逻辑,只有队列有可读数据时,才执行读取操作。
检查可读操作包含了一个预取操作,即在queue.front()与指针r之间的元素称之为“已预取元素”,在取这段区域的元素时,无需对原子量c访问即可直接获取,实现真正的无锁操作。

ypipt_t队列可读检查

4. 结论

  • 一个队列纯粹的数据写入和读取,由于操作的是队列两端不同的数据指针,因此对于一读一写两个线程并发来说,无需加锁操作。
  • 纯粹的数据写入和读取,如果没有对空队列状态的状态进行判断,读取过程可能会越界。
  • 为了读取过程不越界,需要给为其设置一个边界值,供读取线程在读取前访问判断是否越界。为了保证写入的数据都能被读取,写入线程在写入数据后,需要向后设置这个边界。
  • 由于这个边界值时读取线程和写入线程的共享变量,由于编译器优化、CPU乱序执行等原因,该共享变量的读写需要原子化。
  • 原子变量的读写同样会带来较大的性能损耗,为避免每次访问队列都访问这个原子变量边界值,对队列的读取过程采用“预取”逻辑,对写入过程采用“flush”逻辑,即:
    • 读取线程将原子变量边界值一次性移动到最后一个可读元素(并用本地指针暂存),此后的读取过程不再访问原子变量边界,直到原子变量边界之前的元素读完后,再将原子变量批量后移。
    • 写入过程使用逐个写入多个数据,直到最后调用flush将原子变量边界设置到队列尾部。

转载于:https://my.oschina.net/luckysym/blog/680749

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值