源代码分析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; }
// 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; }
在这边的细节如下:
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); }
只要不是访问同一个元素的时候,这种队列允许一个线程push()&&back(),而同时另一个线程pop()&&front()。
5-2我们会分析下ypipe,敬请期待。
希望有兴趣的朋友可以和我联系,一起学习。 [email protected]