zeromq源码分析笔记之无锁队列ypipe_t(3)

在上一篇中说到了mailbox_t的底层实际上使用了管道ypipe_t来存储命令。而ypipe_t实质上是一个无锁队列,其底层使用了yqueue_t队列,ypipe_t是对yueue_t的再包装,所以我们先来看看yqueue_t是怎么实现的。

1、yqueue_t

yqueue_t是一个高效的队列,高效体现在她的内存配置上,尽量少的申请内存,尽量重用将要释放的内存。其实,容器的设计都会涉及这点--高效的内存配置器,像sgi stl容器的内存配置器,使用了内存池,预先分配一块较大内存,用不同大小的桶管理,容器申请内存时从相应的桶里拿一块内存,释放内存时又把内存回收到相应的桶里,这样就能做到尽量少的malloc调用。yqueue_t并没有使用内存池,但是利用了同样的思想,一次性分配一个chunk_t减少内存分配次数,并用spare_chunk管理将要释放的块用于内存回收,详细的实现后面再说,先看一下yqueue_t的整个概况,源码位于Yqueue.hpp

复制代码
    //  T is the type of the object in the queue.队列中元素的类型
    //  N is granularity(粒度) of the queue,简单来说就是yqueue_t一个结点可以装载N个T类型的元素,可以猜想yqueue_t的一个结点应该是个数组
    template <typename T, int N> class yqueue_t
    {
    public:
        inline yqueue_t ();//  Create the queue.
        inline ~yqueue_t ();//  Destroy the queue.   
        inline T &front ();//  Returns reference to the front element of the queue. If the queue is empty, behaviour is undefined.
        inline T &back ();//  Returns reference to the back element of the queue.If the queue is empty, behaviour is undefined.
        inline void push ();//  Adds an element to the back end of the queue.
        inline void pop ();//  Removes an element from the front of the queue.
        inline void unpush ()//  用于回滚操作,暂时先不管这个函数,用到再说
    private:
        //  Individual memory chunk to hold N elements.
        struct chunk_t
        {
             T values [N];
             chunk_t *prev;
             chunk_t *next;
        };
    chunk_t </span>*<span style="color: #000000;">begin_chunk;
    </span><span style="color: #0000ff;">int</span><span style="color: #000000;"> begin_pos;
    chunk_t </span>*<span style="color: #000000;">back_chunk;
    </span><span style="color: #0000ff;">int</span><span style="color: #000000;"> back_pos;
    chunk_t </span>*<span style="color: #000000;">end_chunk;
    </span><span style="color: #0000ff;">int</span><span style="color: #000000;"> end_pos;

    atomic_ptr_t</span>&lt;chunk_t&gt; spare_chunk;  <span style="color: #0000ff;"><strong>//空闲块(<strong>我把所有元素都已经出队的块称为空闲块</strong>),读写线程的共享变量</strong></span>
};</pre>
复制代码

可以看到,yqueue_t是采用双向链表实现的,链表结点称之为chunk_t,每个chunk_t可以容纳N个T类型的元素,以后就以一个chunk_t为单位申请内存,begin_chunk可以理解为链表头结点,back_chunk可以理解为队列中最后一个元素所在的链表结点,我们知道容器都应该要能动态扩容的,end_chunk就是拿来扩容的,总是指向链表的最后一个结点,而spare_chunk表示最近的被踢出队列的链表结点。入队操作back_chunk和back_pos,back_chunk结点填满元素时该扩容,让end_chunk指向新的链表结点或者之前释放的链表结点,出队操作begin_chunk和begin_pos,begin_chunk所有元素都出完后并不释放内存,而是让spare_chunk指向他,然后释放spare_chunk上一次的指针,这样扩容的时候就可以重新使用这个结点了。begin_chunk,begin_pos,back_chunk,back_pos,end_chunk,end_pos的关系大致如下:

这里需要重点说一下spare_chunk,根据上面的描述,扩容(写线程的事)和出队列(写线程的事)都会用到这个变量,所以这个变量是读写共享的,有同步的语义,zmq用了atomic_ptr_t<T>来做同步,atomic_ptr_t同样可以看成是一个指针,结构上atomic_ptr_t内含一个指针,提供了两个原子操作和一个非原子操作,在yqueue_t中就需要用到其中一个原子操作xchg。

复制代码
//  This class encapsulates several atomic operations on pointers.
    template <typename T> class atomic_ptr_t
    {
    public:   
        inline void set (T *ptr_);//非原子操作   
        inline T *xchg (T *val_);//原子操作
        inline T *cas (T *cmp_, T *val_);//原子操作
    private:
        volatile T *ptr;
    }
复制代码
  • set函数,把私有成员ptr指针设置成参数ptr_的值,不是一个原子操作,需要使用者确保执行set过程没有其他线程使用ptr的值
  • xchg函数,把私有成员ptr指针设置成参数val_的值,并返回ptr设置之前的值。原子操作,操作系统保证线程安全
  • cas函数,把私有成员ptr指针与参数cmp_指针比较,如果相等,就把ptr设置为参数val_的值,返回ptr设置之前的值;如果直接返回ptr值。原子操作,操作系统保证线程安全

在实现上xchg和cas函数就是包装了各种cpu提供的xchg和cas原子操作,想了解原理的可以查一查着方面的资料,这里只需要知道有这个功能就可以了。

有了这个指针,就可以保证单个读线程和单个写线程时的线程安全了。

接着,来看下yqueue_t是如何构造、push、pop的。后面我会把begin_chunk和begin_pos合起来成为队头指针,back_chunk和back_pos合起来成为队尾指针,end_chunk和end_pos合起来称为容器指针

①构造yqueue_t

复制代码
        inline yqueue_t ()
        {
             begin_chunk = (chunk_t*) malloc (sizeof (chunk_t));
             alloc_assert (begin_chunk);
             begin_pos = 0;
             back_chunk = NULL;//back_chunk总是指向队列中最后一个元素所在的链表结点,现在还没有元素,所以初始为空
             back_pos = 0;
             end_chunk = begin_chunk;//end_chunk总是指向链表的最后一个结点
             end_pos = 0;
        }
复制代码

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;
            </span><span style="color: #008000;">//</span><span style="color: #008000;">  'o' has been more recently used than spare_chunk,
            </span><span style="color: #008000;">//</span><span style="color: #008000;">  so for cache reasons we'll get rid of the spare and
            </span><span style="color: #008000;">//</span><span style="color: #008000;">  use 'o' as the spare.</span>
            chunk_t *cs =<span style="color: #000000;"> spare_chunk.xchg (o);<strong><span style="color: #3366ff;">//由于局部性原理,总是保存最新的空闲块而释放先前的空闲快
            </span></strong></span><span style="color: #0000ff;">free</span><span style="color: #000000;"> (cs);
        }
    }</span></pre>
复制代码

主要是链表的基本操作。pop虽然只有几行代码,却也有两个点需要注意:

  1. pop掉的元素,其销毁工作交给调用者完成
  2. 空闲块的保存,要求是原子操作。这得想明白为什么。原因是,空闲块是读写线程的共享变量,需要做同步,我们会在push中看到,push使用了spare_chunk。

push

复制代码
        inline void push ()
        {
            back_chunk = end_chunk;
            back_pos = end_pos;
        if (++end_pos !=<span> N)<span style="color: #3366ff;"><strong>//end_pos==N表明这个链表结点已经满了
            </strong></span>return<span>;

        chunk_t *sc =<span> spare_chunk.xchg (NULL);
        if<span> (sc) {
            end_chunk-&gt;next =<span> sc;
            sc-&gt;prev =<span> end_chunk;
        } else<span> {
            end_chunk-&gt;next = (chunk_t*) malloc (sizeof<span> (chunk_t));
            alloc_assert (end_chunk-&gt;<span>next);
            end_chunk-&gt;next-&gt;prev =<span> end_chunk;
        }
        end_chunk = end_chunk-&gt;<span>next;
        end_pos = 0<span>;
    }</span></span></span></span></span></span></span></span></span></span></span></span></span></span></span></pre>
复制代码

push操作并未真正的push一个元素,只是把队尾指针指向容器指针,然后让容器指针加1,所以,这二者的值总是差1,二者的关系在第2节中的四个图中可以看的更清楚。扩容的条件是容器指针到达了容器尾(所以end_chunk是拿来扩容的),扩容时先去spare_chunk拿之前废弃的块(所有元素都被pop的块),拿到了就重用,没拿到就得重新申请。同样需要注意,拿空闲块需要做同步操作。

当end_pos==N时,需要扩容,如下:

④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];
        }
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  Returns reference to the back element of the queue.
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  If the queue is empty, behaviour is undefined.</span>
    inline T &amp;<span style="color: #000000;">back ()
    {
        </span><span style="color: #0000ff;">return</span> back_chunk-&gt;<span style="color: #000000;">values [back_pos];
    }</span></pre>
复制代码

总的来说yqueue_t还是比较好理解,现在可以来看一看ypipe_t的实现。

2、ypipe_t

先看下ypipe_t的介绍(注释)、类继承关系、类接口及数据成员

复制代码
   //  Lock-free queue implementation.
    //  Only a single thread can read from the pipe at any specific moment.
    //  Only a single thread can write to the pipe at any specific moment.
    //  T is the type of the object in the queue.
    //  N is granularity of the pipe, i.e. how many items are needed to
    //  perform next memory allocation.
    template <typename T, int N> class ypipe_t : public ypipe_base_t<T,N>   
template </span>&lt;typename T, <span style="color: #0000ff;">int</span> N&gt; <span style="color: #0000ff;">class</span><span style="color: #000000;"><strong><span style="color: #ff00ff;"> ypipe_base_t</span></strong>
{
</span><span style="color: #0000ff;">public</span><span style="color: #000000;">:
    </span><span style="color: #0000ff;">virtual</span> ~<span style="color: #000000;">ypipe_base_t () {}
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">void</span> write (<span style="color: #0000ff;">const</span> T &amp;value_, <span style="color: #0000ff;">bool</span> incomplete_) = <span style="color: #800080;">0</span><span style="color: #000000;">;
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">bool</span> unwrite (T *value_) = <span style="color: #800080;">0</span><span style="color: #000000;">;
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">bool</span> flush () = <span style="color: #800080;">0</span><span style="color: #000000;">;
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">bool</span> check_read () = <span style="color: #800080;">0</span><span style="color: #000000;">;
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">bool</span> read (T *value_) = <span style="color: #800080;">0</span><span style="color: #000000;">;
    </span><span style="color: #0000ff;">virtual</span> <span style="color: #0000ff;">bool</span> probe (<span style="color: #0000ff;">bool</span> (*fn)(T &amp;)) = <span style="color: #800080;">0</span><span style="color: #000000;">;
};
template </span>&lt;typename T, <span style="color: #0000ff;">int</span> N&gt; <span style="color: #0000ff;">class</span> <strong><span style="color: #ff0000;">ypipe_t</span> </strong>: <span style="color: #0000ff;">public</span> <strong><span style="color: #ff00ff;">ypipe_base_t</span></strong>&lt;T,N&gt;<span style="color: #000000;">
{
</span><span style="color: #0000ff;">protected</span><span style="color: #000000;">:
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  Allocation-efficient queue to store pipe items.
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  Front of the queue points to the first prefetched item, back of the pipe points to last un-flushed item. 
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  Front is used only by reader thread, while back is used only by writer thread.</span>
    yqueue_t &lt;T, N&gt;<span style="color: #000000;"> queue;<span style="color: #339966;">//底层容器

    </span></span><span style="color: #008000;">//</span><span style="color: #008000;">  Points to the first un-flushed item. This variable is used exclusively by writer thread.</span>
    T *w;<span style="color: #0000ff;"><strong>//指向第一个未刷新的元素,只被写线程使用

    </strong></span><span style="color: #008000;">//</span><span style="color: #008000;">  Points to the first un-prefetched item. This variable is used exclusively by reader thread.</span>
    T *r;<span style="color: #0000ff;"><strong>//指向第一个还没预提取的元素,只被读线程使用

    </strong></span><span style="color: #008000;">//</span><span style="color: #008000;">  Points to the first item to be flushed in the future.</span>
    T *f;<span style="color: #0000ff;"><strong>//指向下一轮要被刷新的一批元素中的第一个

    </strong></span><span style="color: #008000;">//</span><span style="color: #008000;">  The single point of contention between writer and reader thread.
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  Points past the last flushed item. If it is NULL,reader is asleep. 
    </span><span style="color: #008000;">//</span><span style="color: #008000;">  This pointer should be always accessed using atomic operations.</span>
    atomic_ptr_t &lt;T&gt; c;<span style="color: #0000ff;"><strong>//读写线程共享的指针,指向每一轮刷新的起点(看代码的时候会详细说)。当c为空时,表示读线程睡眠(只会在读线程中被设置为空)</strong></span>

}

复制代码

ypipe_t继承自ypipe_base_t,其提供了一组操作管道的接口,从ypipe_t源码来看,他只是实现了这组接口,并没有提供其他的方法了。T、N在yqueue_t中已经详细说过了。从数据成员来看,其底层使用了上面讲到的yqueue_t,可以猜想,ypipe_t的write、read等操作往yqueue_t中写数据读数据。ypipe_t开头的的注释中写道,ypipe_t是一个无锁队列的实现,单个读单个写同时操作ypipe_t是线程安全的。有没有发现这就是一个生产者消费者的问题,写线程是生产者,读线程是消费者,yqueue_t就是缓冲区,由于只有一个生产者、一个消费者,并不涉及同类线程间的互斥,只需读线程和写线程同步就可以了,操作系统课程中的解法就是两个信号量+PV操作,涉及到锁,而这里是无锁,其实就是使用了数据成员中的三个指针w、r、c来实现的。至于f指针,是用来保证数据完整性的,zmq中一个完整的数据是可以分成多段往ypipe_t中写的(下面源码的write函数incomplete_参数),只有数据写完整了才允许读线程去读数据,关于这点,可以在session与socket_base_t实例的通信中看到,后面会有文章专门详细介绍,我们前面说过的mailbox_t底层也使用了ypipe_t,但mailbox不会把命令分段,每次都是完整的数据。那么接下来就看看这几个指针时如何协同工作的吧。

先看ypipe_t构造的时候做了什么事情:

复制代码
//  Initialises the pipe.
inline ypipe_t ()
{
    //  Insert terminator element into the queue.
    queue.push ();//yqueue_t的尾指针加1,开始back_chunk为空,现在back_chunk指向第一个chunk_t块的第一个位置
</strong></span></span><span style="color: #008000;">//</span><span style="color: #008000;">  Let all the pointers to point to the terminator.</span>
r = w = f = &amp;<span style="color: #000000;">queue.back ();
c.</span><span style="color: #0000ff;">set</span> (&amp;<span style="color: #000000;">queue.back ());

}

复制代码

在ypipe_t中,back_chunk+back_pos类似vector的end迭代器,上面的注释"Let all the pointers to point to the terminator."也是这个意思,就是让r、w、f、c四个指针都指向这个end迭代器,有关这点在write的时候能看清晰的感受到。那么做完这一步,他们关系像下面这个样子:

ps.后面7个格子都属于同一个chunk_t块(yqueue_t介绍了chunk_t结点内含一个数组)

现在看看如何往queue写数据:

复制代码
inline void write (const T &value_, bool incomplete_)
{
    //  Place the value to the queue, add new terminator element.
    queue.back () = value_;
    queue.push ();
</span><span style="color: #008000;">//</span><span style="color: #008000;">  Move the "flush up to here" poiter.</span>
<span style="color: #0000ff;">if</span> (!<span style="color: #000000;">incomplete_)
    f </span>= &amp;<span style="color: #000000;">queue.back ();

}

复制代码

write往ypipe_t的end迭代器写入内容,然后让end迭代器下移一个位置,参数incomplete_=true表示数据分段,现在写的只是其中一段,当incomplete=false时所有数据段都写完了,把指针f指向end迭代器,所以从w指针到f指针这一段表示一个完整的数据。假设完整的数据为ABC,现在把数据分三段A、B、C写入ypipe_t,调用write的形式为:

复制代码
write(A,true);
some code;
write(B,true);
some code;
write(C,false);
some code;
复制代码

这时w,f,r,c的关系如下图:

当一个完整的数据写完后,写线程会调用flush函数,让读线程看到这个完整的数据,如果读线程睡眠了,写线程有义务唤醒读线程。这里涉及了几个点:

  1. 如何让读线程看到这个数据?
  2. 如何判断读线程睡眠?
  3. 读线程睡眠时,写线程如何通知读线程?
  4. 如何刷新

然后带着这四个问题来看看flush函数的实现:

复制代码
inline bool flush ()
{
     //    If there are no un-flushed items, do nothing.
     if (w == f)
          return true;
 </span><span style="color: #008000;">//</span><span style="color: #008000;">    Try to set 'c' to 'f'.</span>
 <span style="color: #0000ff;">if</span> (c.cas (w, f) !=<span style="color: #000000;"> w) {
       </span><span style="color: #008000;">//</span><span style="color: #008000;">    Compare-and-swap was unseccessful because 'c' is NULL.
       </span><span style="color: #008000;">//</span><span style="color: #008000;">    This means that the reader is asleep. Therefore we don't care about thread-safeness and update c in non-atomic manner.
       </span><span style="color: #008000;">//</span><span style="color: #008000;">     We'll return false to let the caller know  that reader is sleeping.</span>
       c.<span style="color: #0000ff;">set</span><span style="color: #000000;"> (f);
       w </span>=<span style="color: #000000;"> f;
       </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">false</span><span style="color: #000000;">;
  }

  </span><span style="color: #008000;">//</span><span style="color: #008000;">    Reader is alive. Nothing special to do now. Just move the 'first un-flushed item' pointer to 'f'.</span>
  w =<span style="color: #000000;"> f;
  </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;

}

复制代码

可以看到w==f 时,flush是直接返回的,什么也没做,而当write函数的incomplete_=false时,把 f 指向了新的结点,这个时候 w!=f 了,flush函数才有所作为,所以w、f指针合作可用来告知flush函数现在能否刷新。

当w!=f时,真正执行刷新,可以看到所谓的刷新只做了两件事情,c=f和w=f,其中c是读写线程共享的指针,所以用了原子操作cas来完成c=f的功能。cas这个函数我们在queue_t中也说过了,这里有必要再说一下她的大概实现:

复制代码
T* cas(w,f){
    ret=c ;
    if(c==w)
        c = f;
    return ret;
}
复制代码

可以看到c!=w时,设置失败,并返回当前c的值。在写线程中c和w总是指向同一个值的,都是指向f(刷新的目的就是两个赋值,c=f和w=f),只有被读线程改写的情况下c!=w才成立,而读线程只在一种情况下改写c,那就是队列中没有数据时,读线程把c置NULL然后睡眠,这点在说read的源码时会看到。所以,cas的返回值代表了读线程的状态,返回NULL,说明读线程睡眠了,此时cas没有成功,所以需要使用set函数,把c指向f。这样就完成了刷新工作。

先来回答上面的4个问题

  1. 如何让读线程看到这个数据?
    令c=f,读线程会检查指针c,判断是否有数据
  2. 如何判断读线程睡眠?
    c.cas(w,f)返回NULL,读线程睡眠
  3. 读线程睡眠时,写线程如何通知读线程?
    flush函数返回false,表明读线程睡眠了,写线程看到flush返回false之后会发送一个消息给读线程。关于这点可以看上一篇中mailbox的send函数源码
  4. 如何刷新
    c=f ; w=f

再看一下刷新之后,w、f、c、r的关系:

再来看一下读线程如何read:

复制代码
//  Reads an item from the pipe. Returns false if there is no value available.
inline bool read (T *value_)
{
    //  Try to prefetch a value.
    if (!check_read ())
          return false;
</span><span style="color: #008000;">//</span><span style="color: #008000;">  There was at least one value prefetched.Return it to the caller.</span>
*value_ =<span style="color: #000000;"> queue.front ();
queue.pop ();
</span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;

}

复制代码

可以看到,read函数会先检查队列中是否有数据可读,如果没有数据可读直接就返回了,如果有数据可读,会在check_read中预取数据。这里面有两个点,一个是检查是否有数据可读,一个是预取,所以带着这两个问题来看看check_read函数的源码:

复制代码
//  Check whether item is available for reading.
inline bool check_read ()
{
    //  Was the value prefetched already? If so, return.
    if (&queue.front () != r && r)//判断是否在前几次调用read函数时已经预取数据了return true;
</span><span style="color: #008000;">//</span><span style="color: #008000;">  There's no prefetched value, so let us prefetch more values.
</span><span style="color: #008000;">//</span><span style="color: #008000;">  Prefetching is to simply retrieve the </span><span style="color: #008000;">pointer from c in atomic fashion.
</span><span style="color: #008000;">//</span><span style="color: #008000;">  If there are no items to prefetch, set c to NULL (using compare-and-swap).</span>
r = c.cas (&amp;<span style="color: #000000;">queue.front (), NULL);<span style="color: #0000ff;"><strong>//尝试预取数据

</strong></span></span><span style="color: #008000;">//</span><span style="color: #008000;">  If there are no elements prefetched, exit.
</span><span style="color: #008000;">//</span><span style="color: #008000;">  During pipe's lifetime r should never be NULL, however,</span><span style="color: #008000;">it can happen during pipe shutdown when items </span><span style="color: #008000;">are being deallocated.</span>
<span style="color: #0000ff;">if</span> (&amp;queue.front () == r || !<span style="color: #000000;">r)<strong><span style="color: #0000ff;">//判断是否成功预取数据
     </span></strong></span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">false</span><span style="color: #000000;">;

</span><span style="color: #008000;">//</span><span style="color: #008000;">  There was at least one value prefetched.</span>
<span style="color: #0000ff;">return</span> <span style="color: #0000ff;">true</span><span style="color: #000000;">;

}

复制代码

可以看到,check_read是通过指针r的位置来判断是否有数据可读的:如果指针r指向的是队头元素(r==&queue.front())或者r没有指向任何元素(NULL)则说明队列中并没有可读的数据,这个时候check_read尝试去预取数据。所谓的预取就是令 r=c (cas函数就是返回c本身的值,看上面关于cas的实现), 而c在write中被指向f(见上图),这时从queue.front()到f这个位置的数据都被预取出来了,然后每次调用read都能取出一段。值得注意的是,当c==&queue.front()时,代表数据被取完了,这时把c指向NULL,接着读线程会睡眠,这也是给写线程检查读线程是否睡眠的标志。

继续上面写入ABC数据的场景,第一次调用read时,会先check_read,把指针r指向指针c的位置(所谓的预取),这时r,c,w,f的关系如下:

这时,&queue.front()!=r,读线程的就可以一直读数据了,直到队头到达了指针r的位置,表示没数据了,然后陷入睡眠。

综上,就是ypipe_t无锁队列的实现,再总结一下过程:

数据可分段,写线程一次写入一段,所有数据都写完后把f指向队列的end迭代器位置,用以表示下一轮的写位置,然后flush,把c和w指向end的位置,通知读线程,读线程check_read预取数据,把r也指向end位置,每次read的时候队头指针都下移一个位置,直到队头移动到r的位置,也就是end,表示没数据了,读线程把c指针置空,表示数据我都读完了,我要睡了,下次再喊我。

下一篇将介绍session与socket_base_t的消息通信,还要一段时间再更新。

写者:zengzy
出处: http://www.cnblogs.com/zengzy
标题有【转】字样的文章从别的地方转过来的,否则为个人学习笔记

<a href="https://www.cnblogs.com/zengzy/p/5132437.html" class="p_n_p_prefix">« </a> 上一篇:    <a href="https://www.cnblogs.com/zengzy/p/5132437.html" title="发布于 2016-01-15 21:00">zeromq源码分析笔记之线程间收发命令(2)</a>
<br>
<a href="https://www.cnblogs.com/zengzy/p/5139582.html" class="p_n_p_prefix">» </a> 下一篇:    <a href="https://www.cnblogs.com/zengzy/p/5139582.html" title="发布于 2016-01-19 17:22">环形缓冲区的设计及其在生产者消费者模式下的使用(并发有锁环形队列)</a>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值