首次听到"无锁队列"时,总会有这样的印象:多线程读写且不需要同步的强大的数据结构。当然,这是不可能的。
所谓的无锁队列,非但不是无锁的,往往还有很多条件限制。
zeromq源码yqueue.hpp注释,已经说明了一切。
// yqueue is an efficient queue implementation. The main goal is
// to minimise number of allocations/deallocations needed. Thus yqueue
// allocates/deallocates elements in batches of N.
//
// yqueue allows one thread to use push/back function and another one
// to use pop/front functions. However, user must ensure that there's no
// pop on the empty queue and that both threads don't access the same
// element in unsynchronised manner.
yqueue的使用是先back/front进行写读操作,再通过push/pop更新数据结构。一开始看到注释里push/back、pop/front还挣扎了一段时间,以为是先push/pop再back/front进行操作的。
见zeromq中yqueue的使用,ypipe.hpp:
inline void write (const T &value_, bool incomplete_)
{
// Place the value to the queue, add new terminator element.
queue.back () = value_;
queue.push ();
// Move the "flush up to here" poiter.
if (!incomplete_)
f = &queue.back ();
}
front/pop的使用顺序同样可以在zeromq的源码找到。
为什么一个线程读,另一个线程写不需要进行同步呢?其实不需要同步这种说法是错误的,实际上也是做了同步的。
back和front方法通过back_pos 和begin_pos来定位元素,当然不需要同步,但pop和push方法访问了同一变量spare_chunk,是需要同步的。
spare_chunk中的xchg和CAS即是同步。
#elif defined ZMQ_ATOMIC_PTR_X86
T *old;
__asm__ volatile (
"lock; cmpxchg %2, %3"
: "=a" (old), "=m" (ptr)
: "r" (val_), "m" (ptr), "0" (cmp_)
: "cc");
return old;
其实在单核环境下,CAS也不需要lock 只有在SMP环境下,才需要。
DPDK中ring buff也是类似实现。