源代码分析5主要分析管道相关的数据结构yqueue, ypipe, pipe等。
由于篇幅影响,我们一个个来分析,先看yqueue:
yqueue是一个高效的队列实现。它主要通过批量的分配/释放数据元素来减少分配/释放的次数来提高效率。
而所谓的批量分配的数据结构称之为chunk_t:
// Individual memory chunk to hold N elements.
struct chunk_t
{
T values [N]; // 元素类型的N长度的数组
chunk_t *prev; // 前驱元素的指针
chunk_t *next; // 后驱元素的指针
};
而queue通过多个chunk_t形成双向链表。
因为我们知道队列的基本操作就是入队和出队,即队尾push()和队头pop():
所以还有一些数组索引和chunk指针来跟踪链表元素:
// Back position may point to invalid memory if the queue is empty,
// while begin & end positions are always valid. Begin position is
// accessed exclusively be queue reader (front/pop), while back and
// end positions are accessed exclusively by queue writer (back/push).
chunk_t *begin_chunk; // 指向队头chunk_t的指针
int begin_pos; // 在队头chunk_t中队首元素在数组values中的索引位置
chunk_t *back_chunk; // 指向队尾chunk_t的指针
int back_pos; // 在队尾chunk_t中队尾元素在数组values中的索引位置
chunk_t *end_chunk; // push之后的chunk_t的结构指针
int end_pos; // push之后的chunk_t的元素在数组values中的索引位置
基本上的工作原理如下:
首次分配一个chunk_t,这样就一次性分配了N个元素,而此时begin_chunk和end_chunk都指向这个chunk_t,back_chunk就指向NULL, 而所有的postion都为0:
// Create the queue.
inline yqueue_t ()
{
begin_chunk = (chunk_t*) malloc (sizeof (chunk_t));
alloc_assert (begin_chunk);
begin_pos = 0;
back_chunk = NULL;
back_pos = 0;
end_chunk = begin_chunk;
end_pos = 0;
}
而我们可以分别调用front()和back()函数来获得队首和队尾元素的引用,以便修改它们。
// Returns reference to the front element of the queue.
// If the queue is empty, behaviour is undefined.
inline T &front ()
{
return begin_chunk->values [begin_pos];
}
// Returns reference to the back element of the queue.
// If the queue is empty, behaviour is undefined.
inline T &back ()
{
return back_chunk->values [back_pos];
}
刚创建好的queue是一个只有一个元素的队列,该元素被认为是队首元素,之所以back_chunk指向NULL,也是为了表明现在的队列中木有队尾元素。(反正zeromq是这么做的。。。)
对于yqueue的操作如果要添加一个元素入队,那么我们就要先调用push()函数,再调用back()函数获得该队尾元素的引用,然后操作该元素。
接下来我们看看push()函数的实现:
// Adds an element to the back end of the queue.
inline void push ()
{
back_chunk = end_chunk;
back_pos = end_pos;
if (++end_pos != N)
return;
chunk_t *sc = spare_chunk.xchg (NULL);
if (sc) {
end_chunk->next = sc;
sc->prev = end_chunk;
} else {
end_chunk->next = (chunk_t*) malloc (sizeof (chunk_t));
alloc_assert (end_chunk->next);
end_chunk->next->prev = end_chunk;
}
end_chunk = end_chunk->next;
end_pos = 0;
}
在这边的原则就是不断递增移动end_pos的位置,如果到达了N,说明当前chunk_t中的数组已经木有元素空间了,因为最大元素就N-1,所以在这种情况下就得去分配一个新的chunk_t,并且接到双向链表的尾部。
在这边的细节如下:
1. end_pos永远比back_pos在逻辑上前进一格,如果在同一个chunk_t的数组中就是end_pos比back_pos大1,而如果end_pos由于到达N之后需要新建一个chunk_t的时候,就跳跃到那个新的chunk_t中计索引为0。
2. end_chunk中是预取的chunk_t,而back_chunk是当前队列结尾的chunk_t。
3. 这边有一个spare_chunk,这个chunk相当于cache的作用,后面我们会讲pop()函数,然后你会发现spare_chunk保存在最近被释放的chunk,以便push()的时候需要新加chunk的时候可以复用。
下面我们来看下pop()函数:
// Removes an element from the front end of the queue.
inline void pop ()
{
if (++ begin_pos == N) {
chunk_t *o = begin_chunk;
begin_chunk = begin_chunk->next;
begin_chunk->prev = NULL;
begin_pos = 0;
// 'o' has been more recently used than spare_chunk,
// so for cache reasons we'll get rid of the spare and
// use 'o' as the spare.
chunk_t *cs = spare_chunk.xchg (o);
if (cs)
free (cs);
}
}
一般情况下就递增begin_chunk的数组的begin_pos,如果到达了N的话我们就需要抛弃当前这个chunk_t,并且交换到spare_chunk中,然后跳跃到next的chunk_t,并且
重置begin_pos为0。spare_chunk以前指向的chunk_t会被释放空间。
还有一个rollback push()操作的函数unpush():
// Removes element from the back end of the queue. In other words
// it rollbacks last push to the queue. Take care: Caller is
// responsible for destroying the object being unpushed.
// The caller must also guarantee that the queue isn't empty when
// unpush is called. It cannot be done automatically as the read
// side of the queue can be managed by different, completely
// unsynchronised thread.
inline void unpush ()
{
// First, move 'back' one position backwards.
if (back_pos)
--back_pos;
else {
back_pos = N - 1;
back_chunk = back_chunk->prev;
}
// Now, move 'end' position backwards. Note that obsolete end chunk
// is not used as a spare chunk. The analysis shows that doing so
// would require free and atomic operation per chunk deallocated
// instead of a simple free.
if (end_pos)
--end_pos;
else {
end_pos = N - 1;
end_chunk = end_chunk->prev;
free (end_chunk->next);
end_chunk->next = NULL;
}
}
该函数主要就是回退,如果back_pos,end_pos为0的话,表示当前位置指向一个新的chunk_t的数组首个元素,于是就要回退到上一个chunk_t的数组末尾元素,即values[N-1]。否则的话就递减position。至于析构函数就是销毁双向链表的操作:
// Destroy the queue.
inline ~yqueue_t ()
{
while (true) {
if (begin_chunk == end_chunk) {
free (begin_chunk);
break;
}
chunk_t *o = begin_chunk;
begin_chunk = begin_chunk->next;
free (o);
}
chunk_t *sc = spare_chunk.xchg (NULL);
if (sc)
free (sc);
}
yqueue的线程安全性:
只要不是访问同一个元素的时候,这种队列允许一个线程push()&&back(),而同时另一个线程pop()&&front()。
5-2我们会分析下ypipe,敬请期待。
希望有兴趣的朋友可以和我联系,一起学习。 kaka11.chen@gmail.com