zeromq源代码分析5-1------管道相关的数据结构yqueue, ypipe, pipe等

来源:互联网 发布:软件项目人员管理制度 编辑:程序博客网 时间:2024/06/14 15:28

源代码分析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