1. 综述
ypipe_t是zmq中的重要的数据结构。它实现了一个无锁的队列,支持单一的读线程和单一的写线程能够在无锁的情况下并发执行。ypipe_t是zmq进行消息发送的最底层数据结构,写线程会把消息写入到ypipe_t中,读线程会从ypipe_t中读消息。在ypipe_t上层zmq又封装了更易用的数据结构,要想了解更上层的数据结构,需要首先把ypipe_t原理搞清楚。
本文主要介绍了ypipe_t代码实现上的继承关系,相关的类的实现细节,以及关键函数的流程说明。
更上层的封装在后续文章中说明。
2. ypipe_t继承关系
3. 类及关键函数说明
3.1. atomic_ptr_t
atomic_ptr.hpp是zmq中的一个原子指针的实现,主要用在ypipe_t及yqueue_t结构中实现无锁队列。
template <typename T> class atomic_ptr_t
{
public:
atomic_ptr_t () //默认构造函数,会把内部指针ptr设置为null。
~atomic_ptr_t () //析构函数,什么操作都没有。
void set (T *ptr_) /*设置内部指针ptr为ptr_。
非线程安全函数,在使用的时候需要用户自己保证没有其他的线程正在操作ptr_。*/
T *xchg (T *val_) //线程安全,原子操作,把val_赋值给内部成员变量ptr_,并返回之前旧的ptr_。
inline T *cas (T *cmp_, T *val_)
/*线程安全函数,原子操作。
对象内部变量ptr与cmp_做比较,如果相等,则把val_赋值给ptr,并返回旧的ptr的值。
如果不相等,则ptr不发生变化,返回ptr的值。
*/
}
3.2 yqueue_t
- yqueue_t是一个模板类。其内部实现了一个链表结构用于存储数据。在分配内存的时候yqueue_t并不是一次分配一个数据大小的内存,而是根据模板参数N,一次分配N个数据大小的内存,这样做减少了alloc/dealloc的次数,提高了效率。
- yqueue_t中的元素出队列的时候其内存并不删除,而是返回到了空闲区,在下次有数据入队的时候首先查看空闲区域是否有内存区,如果有的话那么直接使用,不重新分配内存,如果没有空闲去再重新申请内存。
- yueue_t允许同时一个线程push/back,另外一个线程pop/front。但需要保证不能在空队列上pop(具体原因后续说明)。yueue_t两个线程不会同时操作同一个元素(如何保证两个线程不会操作同一个元素?后续说明)。
//T为元素类型,N为一个整形值,一次申请N*T大小的内存。
//在c++中,模板参数中的int值在模板内部就相当于一个常量,因此可以使用N作为模板内部的数组长度参数。
template<typename T, int N> class yqueue_t {
public:
inline yqueue_t(); //构造函数。
inline ~yqueue_t(); //析构函数。
//释放内部数据链表占用的空间。
inline T& front();
//返回链表头元素。读线程使用pop/front。
inline T& back();
//返回链表尾元素。写线程使用push/back。
//添加一个位置到链表尾,此函数并不真正加入元素,而是在链表尾预留出了一个位置给待添加的元素。
inline void push ()
{
back_chunk = end_chunk;
back_pos = end_pos; //因为end_pos总是在back_pos下一个位置,所以此代码实现的其实就是back_pos往后挪了一个位置,给待添加元素留出了一个空间。
//如果仍然有存储T的位置,那么即使调用了push操作,也不会追加chunk_t空间。
if (++end_pos != N) //end_pos总是会在back_pos的后一个位置。
return;
//把一个新的chunk_t添加到chunk_t链表中。
chunk_t *sc = spare_chunk.xchg (NULL); //如果有之前被释放了的chunk_t结构,那么复用。
if (sc) {
//如果有之前被释放的chunk_t结构,则追加到链表中。
end_chunk->next = sc;
sc->prev = end_chunk;
} else {
//如果没有之前释放的chunk_t结构,则重新分配一个chunk_t,然后追加到链表中。
end_chunk->next = allocate_chunk();
alloc_assert (end_chunk->next);
end_chunk->next->prev = end_chunk;
}
//更新相关指针与变量。
end_chunk = end_chunk->next;
end_pos = 0;
}
//回滚最后一次push操作,返回来的元素空间需要由调用者负责释放。
//调用者必须保证queue不为空。(为什么queue不能为空?为空会发生什么?后续研究)
inline void unpush ()
{
//如果back_pos不为0,则减1;如果back_pos为0,则退回到上一个chunk_t的最后一个位置。
if (back_pos)
--back_pos;
else {
back_pos = N - 1;
back_chunk = back_chunk->prev;
}
//end_pos不为0,则减1;否则end指针移动到前一个chunk_t块,然后释放最后一个chunk_t块。
if (end_pos)
--end_pos;
else {
end_pos = N - 1;
end_chunk = end_chunk->prev;
free (end_chunk->next);
end_chunk->next = NULL;
}
}
//从队列头取出一个元素。
inline void pop ()
{
//chunk_t链表的第一个块已经不被使用了,从链表中移除,并清空相关字段。
if (++ begin_pos == N) {
chunk_t *o = begin_chunk;
begin_chunk = begin_chunk->next;
begin_chunk->prev = NULL;
begin_pos = 0;
chunk_t *cs = spare_chunk.xchg (o); //刚释放的chunk_t指针被置换到spare_chunk中,以备后续需要再追加空间的时候使用。
//把原来保存的chunk_t块释放。
free (cs);
}
}
private:
//双向链表结构体,存储实际的元素。
struct chunk_t
{
T values [N]; //N个T元素的数组。
chunk_t *prev;//前向指针。
chunk_t *next;//后向指针。
};
//一次分配N×T个buffer,并返回chunk_t指针。
chunk_t *allocate_chunk ();
//begin_chunk和begin_pos一起作为begin位置。
chunk_t *begin_chunk; //当前的chunk_t链表头。
int begin_pos; //当前第一个T元素位置。
//back_chunk和back_pos一起作为back位置。
chunk_t *back_chunk; //最后一个chunk_t位置。
int back_pos; //最后一个元素位置。
//end_chunk和end_pos一起作为end位置。
chunk_t *end_chunk; //当前的chunk_t链表尾。
int end_pos; //当前的最后一个元素在chunk_t块中的位置。
atomic_ptr_t<chunk_t> spare_chunk; //由于pop操作被释放的chunk_t块指针,没有被free,会被存储在此指针中,等待下次重用,只存储最后一块被“释放”的chunk_t。
//禁止拷贝赋值操作符及拷贝构造函数。
yqueue_t (const yqueue_t&);
const yqueue_t &operator = (const
yqueue_t&);
3.3 ypipe_base_t
//抽象基类,主要是为了提供接口定义。
template <typename T> class ypipe_base_t
{
public:
//析构函数声明为虚拟函数的好处是派生类可以重载析构函数来释放派生类特有的结构。
virtual ~ypipe_base_t () {}
virtual void write (const T &value_, bool incomplete_) = 0;
virtual bool unwrite (T *value_) = 0;
virtual bool flush () = 0;
virtual bool check_read () = 0;
virtual bool read (T *value_) = 0;
virtual bool probe (bool (*fn)(const T &)) = 0;
};
3.4 ypipe_t
- 重点来了。ypipe_t是上层直接使用的数据结构,其封装了yqueue_t结构。
- ypipe_t是一个无锁队列的实现,单个读单个写同时操作ypipe_t是线程安全的。一个读线程和一个写线程其实为一个生产者消费者问题,写线程为生产者,读线程为消费者,通常有锁的解决冲突的方法为pv操作,这里实现了无锁队列,实际上就是使用w,r,c三个指针来实现的。至于f指针,是用来保证数据完整性的,对于几个指针的使用后面会详细说明。zmq中一个完整的数据是可以分成多个段往pipe中写的,只有一个消息的所有片段写完才允许读线程去读(至于所有片段都写完了读线程才能看到这个消息)。每次写完一个数据(多片或者单片),f指针都会被更新。
//无锁队列的实现。使用时需要保证同一时刻只有一个写线程(不能有多个写线程),只有一个读线程(不能有多个读线程)。
//T是pipe中的元素类型,N为元素的个数(对应于yqueue_t中的N,关于yqueue_t中N的作用请查看3.2节。)
template <typename T, int N> class ypipe_t : public ypipe_base_t <T> {
public:
//构造函数。
inline ypipe_t ()
{
queue.push (); //自行此操作之后,back_pos为0,end_pos为1。
r = w = f = &queue.back ();
c.set (&queue.back ()); //c指向第0个元素。
}
//析构函数。
inline virtual ~ypipe_t () {
}
//向pipe中写入T元素。
//参数:
// value_,待写入的T元素。
// incomplete_,是否有后续元素,true,还有后续元素;false,没有后续元素。
inline void write (const T &value_, bool incomplete_)
{
//back指向当前有效元素的下一个位置。
queue.back () = value_; //插入元素。
queue.push (); //在队列尾增加一个位置。
//一个元素的所有片段都已经写入之后,更新f的值指向back位置。
if (!incomplete_)
f = &queue.back ();
}
}