ZeroMQ源码阅读阶段性总结


Data Structor —— ZMQ的最快底层

关于这部分,我只把其当作我阅读其他部分代码时的前置技能来学习——专注于接口和使用方法,尽量避免看得过深过细,所以这部分只提供几份很好的参考资料,相信这两篇资料能解释包括底层原理在内的众多优化技巧与基本使用方法:
zeromq源码分析笔记之无锁队列ypipe_t(3)- 曾志优
Internal Architecture of libzmq
zmq源码阅读笔记之基础数据结构 - susser43


Own tree —— ZMQ的安全销毁机制

own_t 基类

位置:own_t.hpp, own_t.cpp
简述:是形成own层级结构的所有对象的基类,该类负责这种对象的初始化和销毁。
初始化:提供了两种初始化方法,分别用于IO进程内外的对象的初始化。作为object_t的子类,同样具有收发command的能力。

概念-对象树
提供明确的初始化和销毁机制(主要是销毁机制)。基本原理是:对象树上的每一个对象在销毁自己之前,必须要收到它的所有子对象的销毁确认信息(即所有子对象都已销毁)。

主要变量列表

  • bool _terminating:用于标记,此标记为true时,表示此对象的销毁流程已开始。不过此对象只有在没有子对象或未处理的销毁确认信息时才会被真正销毁。
  • atomic_counter_t set_seqnum & uint64_t prosessed_seqnum:用于计数,记录此对象已收到和已处理的command数。
  • std::set<own_t *> _owned:用于记录,保存此对象的所有子对象。
  • int _term_acks:用于记录,表示可销毁此对象之前必须要接收的_term_ack事件数。(在调用时,往往伴随着发出term命令)

void inc_seqnum()
当层级结构中的另一个对象给此对象发出command的时候,会调用此对象的该函数,以保证该对象在销毁时完成了所有收到的command。机制:用时钟组件记录下当前收到的command数量,在销毁对象前检验是否处理完了所有的command,只有处理完所有command之后才能被销毁,以防止底层的command被丢弃。(set_seqnum & prosessed_seqnum用于记录收到和已处理的command数)

当一个对象进入销毁流程,有以下几种情况
• 父对象发出销毁命令,子对象销毁后向父对象发出销毁确认(term_ack);
• 子对象试图销毁自己(例如tcp连接断开时,tcp会话的关闭),子对象向父对象发出销毁请求,父对象再向子对象发出销毁命令;
• 子对象和父对象同时决定销毁此子对象时,父对象会直接忽略子对象的销毁请求,向子对象发出销毁命令。

当一个对象在进行最终的销毁之前,需要确认以下几点
• 此对象是否在销毁流程中;
• 是否已处理了所有已收到的command;
• 子对象是否已经全部被销毁。
只有满足以上三点,就会被立刻删除(delete)

参考资料

Internal Architecture of libzmq - zeromq


Command Flow —— ZMQ如何实现内部的命令传递

序:ZMQ中command与message的区别

在ZMQ中,信息只具有以上两种形式——command和message。而ZMQ作为消息中间件,最主要的功能就是传递信息(message);而ZMQ作为一个消息中间件,也需要多个线程中多个组件的相互配合,而组件与组件之间利用命令(command)进行沟通。换言之,如果将ZMQ比作一家快递公司,message便是客户(应用程序线程)寄出的快递包裹,而command则是公司里员工与员工之间的沟通:“装箱完成,发车吧”。

因此,可以说是command流完成了几乎整个ZMQ的内部信息沟通机制,组件通过发送、接收和处理command控制着自己与其他组件的连接、断开和“生老病死”。所以我在第一部分介绍的内容就是command flow。

基类:command_t —— 真正的命令

位置:command.hpp
概述:一个command由三部分组成:分别是发往的目的地destination,命令的类型type,命令的参数args。所谓的命令就是一个对象交代另一个对象去做某件事情,说白了就是告诉另一个对象应该调用哪个方法,命令的发出者是一个对象,而接收者是一个线程,线程接收到命令后,根据目的地派发给相应的对象做处理。
种类:command只有18种可能的情况;看到这,可能绝大多数命令的意义看起来都是不知所云,不过没关系,这些意义仅在之后的源码分析中作为对照表使用~现在只需了解command的种类只有18种就可以。

1. stop:发给io thread以终止该线程;
2. plug:发送给io object以使其在所在的io thread注册;
3. own:发送给socket以让它知道新创建的对象,并建立own关系;
4. attach:将引擎engine连接至会话session,如果engine为null的话,告知session失败;
5. bind:从会话session发送到套接字socket以在他们之间建立管道pipe,调用者事先调用了inc_seqnum发送命令(增加接收方中的计数 sent_seqnum);
6. activate_read:由pipe wirter告知pipe reader管道中有message;
7. activate_write:由pipe reader告知pipe writer目前已读取的message数;
8. hiccup:创建新inpipe之后由pipe reader发送给writer,它的类型应该是pipe_t::upipe_t,然而这个类型的定义是private的,所以我们使用void*定义;
9. pipe_term:由pipe reader发送给pipe writer以使其term其的末端;
10. pipe_term_ack:pipewriter确认pipe_term命令;
11. pipe_hwm:由一个pipe发送到另一部分以修改hwm(高水位线);
12. term_req:由一个i/o object发送给套接口以请求关闭该i/o object;
13. term:由套接口发送给i/o object以启动;
14. term_ack:由i/o object发送给套接口以确认其已经关闭;
15. term_endpoint:由session_base(I/O thread)发送到套接字(app thread)以请求断开端点。
16. reap:将已closed的socket的所有权转移到reaper thread;
17. reaped:已closed的socket通知reaper thread它已经被解除分配;
18. done:当所有的socket都成功被解除分配之后,由收割者线程发送给term thread。

mailbox_t的前置技能:mutex_t(同步锁)

位置:mutex.hpp
概述:为防止多个线程同时访问同一块数据造成的数据错乱,系统库引入了互斥变量的概念,让每个线程都按顺序地访问变量。
变量
RTL_CRITICAL_SECTION _cs:该锁所对应的临界区。
接口
• inline void lock():将此组件对应的临界区设为受限状态,在该种状态下,此临界区只能由此组件对应的线程访问。
• inline bool try_lock():返回一个bool值表示是否访问成功。
• inline void unlock():放弃当前线程对锁定部分的所有权。一旦锁定部分的所有权被放弃,那么请求访问临界区的下一个线程,将可以对锁定部分进行操作。
• inline RTL_CRITICAL_SECTION* get_cs():返回变量_cs,即对应的临界区。
注意 ❗
假设线程A与线程B是同时访问临界区CriticalSection的两个线程,这个过程实际上是通过限制有且只有一个函数进入临界区CriticalSection来实现代码同步的。简单地说,对于同一个CriticalSection,当一个线程执行了lock()而没有执行unlock()的时候,其它任何一个线程都无法完全执行lock()而不得不处于挂起状态。再次强调一次,没有任何资源被“锁定”,CriticalSection这个临界区不是针对资源的,而是针对不同线程间的代码段的!我们能够用它来进行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了lock()和unlock()语句,使得同一时间只能够有一个线程的代码访问到该临界区而已(其它想访问该资源的代码不得不等待)。
参考资料
什么是临界区资源?对临界区管理的基本原则是什么? - 百度知道
临界区与锁 - joannae
线程同步之临界区 - One heart

mailbox_t的前置技能:signaler_t(信号机)

位置:signaler.hpp, signaler.cpp
概述:用于在两个线程之间发送信号,但是一定要注意:信号机中的任何时刻内都至多只能有一个信号,换言之,在接收到信号之前就给另一端发送信号是错误的。
主要变量列表
• fd_t _w, r:分别是用于发送信号的write标识符和用于读取信号的read标识符,初始化出问题时标识符值为-1。
主要接口列表
• fd_t get_fd():获得read标识符。
• void send():向write处发送信号。
• int wait(int timeout
):等待信号,返回值:错误返回-1,正确返回0。
• void recv ():接收信号,如果出错程序退出。
• int recv_failable ():接收信号,如果发生错误返回-1,运行正常返回0。与recv()的区别就是recv()不会返回值,错误发生程序直接退出;此函数会返回错误值,而不会导致程序退出。
备注:信号机在代码中也包含了在多种不同平台上的不同实现,但不同于poll模式,信号机的不同实现之间只有兼容性的区别,没有性能上的明显区别。

核心类:mailbox_t —— 命令的邮局

位置:mailbox.hpp, mailbox.cpp, i_mallbox.hpp(找不到)
简述:是每一个真正的thread中处理命令流的组件。
主要变量列表
• cpipe_t _cpipe:cpipe即command pipe,是用于存储真正的命令的管道,实现了单一读/写线程的无锁访问。(其中粒度的含义为,每一次内存操作会在底层的pipe中分配出16个command_t的size的内存空间)
• signaler_t _signaler(需要前置技能):从writer管道到reader管道用于通知命令到达的信号机。
• mutex_t _sync(需要前置技能):只有一个线程从信箱接收命令,但是有任意数量的线程向信箱发送命令,由于ypipe_t需要在它的两个端点进行同步访问,所以我们需要同步发送端。
• bool active:command pipe是否是可用的。
主要接口
• fd_t get_fd():(备注typedef SOCKET fd_t)得到信箱所对应的读文件标识符。
• bool valid():检测mailbox的有效性,本质上就是检测信号机的有效性。
• void send(const command_t &cmd
):发送命令。在实际使用的过程中其实是首先找到目的mailbox对象,然后调用此对象的send函数,把消息放入到这个对象的queue中。并不是从一个mailbox能够发送消息到另外一个mailbox。send的含义更像是把消息放入到函数所属对象的queue中。

void zmq::mailbox_t::send (const command_t &cmd_)
{
   
   
    _sync.lock (); //加锁
    _cpipe.write (cmd_, false); //写消息到管道。
    const bool ok =
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值