brpc组件bvar源码解析(二)AgentGroup和AgentCombiner

上一篇:brpc组件bvar源码解析(一)简介、使用和类的关系

1.AgentGroup

AgentGroup中通过tls数据实际保存了bvar在每个线程中的统计值。
AgentGroup类的定义:
在这里插入图片描述
模板参数Agent实际传入的是AgentCombiner::Agent这个结构体;对于Agent相同的bvar,例如两个bvar::Adder< int >,在AgentGroup中实际的存储空间是同一块buffer,通过AgentId定位(类似一个数组的不同下标)。

成员变量都是static的,因为对于一种Agent,这些成员在全局只需要一份定义:
在这里插入图片描述
其中ThreadBlock是AgentGroup的嵌套类:在这里插入图片描述

_s_agent_kinds:标记下一个可用的AgentId是多少;每次分配了新的AgentId后+1
_s_free_ids:bvar在析构时会归还AgentId,归还的AgentId都会放在_s_free_ids中由于重复使用
_s_tls_blocks:是ThreadBlock*的动态一维数组;而ThreadBlock内部是Agent的静态一维数组_agent,数组_agent长度约为4096/sizeof(Agent)(记为L)。_s_tls_blocks一次申请L个agent的buffer,并且因为是动态数组,可以动态扩展
_s_mutex:是保护_s_agent_kinds和_s_free_ids的锁;

函数create_new_agent:

在这里插入图片描述
这是新建一个agent时调用的,目的是获取一个AgentId(实际是int)。一个agent在所有tls的buffer数组中的位置是相同的。这个AgentId就标识了这个位置。它是AgentCombiner的构造函数调用的。AgentCombiner有了这个id,就能把一个bvar在各个tls中的数据定位到,进而“累加”起来。

函数destroy_agent

同理bvar析构时将调用此函数,用于回收AgentId、并暂存到_s_free_ids中。略。

函数get_tls_agent:

在这里插入图片描述
目的是根据AgentId从当前线程的tls数据中获取Agent存储位置的指针。具体是根据AgentId计算出在_s_tls_blocks的下标block_id(因为_s_tls_blocks每个元素指向的ThreadBlock的大小是固定的,即ELEMENTS_PER_BLOCK),然后再计算出在第A个ThreadBlock中的下标B,即得到Agent地址。其实把整个_s_tls_blocks的存储看成一维数组就很好理解了。

这里有个前提条件就是_s_tls_blocks不是空指针,_s_tls_blocks是一个tls数据,如果它为空,get_tls_agent将会返回NULL,此时调用地方将会调用get_or_create_tls_agent函数。

函数get_or_create_tls_agent

在这里插入图片描述
每个线程第一次调用时都会为_s_tls_blocks new出新的空间,并resize足够大;同样根据AgentId计算出block_id,为第block_id个block申请空间;再计算出在block_id个block的下标B,返回B的地址;

2.AgentCombiner

AgentCombiner类的定义:
在这里插入图片描述
三个模板参数:
ResultTp:已经处理好的结果的类型
ElementTp:待处理的元素的类型。
BinaryOp:在聚合所有tls数据时的op操作,例如Adder的op是AddTo(相加),Maxer的op是MaxTo(取最大值),Miner的op是MinTo(取最小值)

ResultTp和ElementTp可能是同一个类型(例如都是int),也可能不同(例如是Stat,是uint64_t,op是AddToStat、负责将uint64_t值加到Stat变量中)。

嵌套类Agent:

在这里插入图片描述

Agent中最重要的就是保存了ElementTp类型的值,即一个bvar的元素类型的值。ElementContainer的封装主要是为了线程安全的操作ElementTp值,对于整型和浮点型,其实就是atomic,其他类型就是读写操作加了Lock。

一个bvar在每个线程tls中实际存储的就是Agent类型。Agent其次保存了AgentCombiner(上面的self_type是AgentCombiner的别名)指针,因为上面介绍AgentGroup时我们知道底层存储是多个Agent一维数组,所以每个Agent需要知道它所属于哪个AgentCombiner。

同时,由于Agent继承自LinkNode,它就有了父类中的指向前一个节点的指针previous_和指向后一个节点的指针next_,成为了双向链表的节点类型。下面介绍_agents时会讲到作用。

成员变量:

在这里插入图片描述
_id:此bvar的AgentId,用于在AgentGroup中查询当前线程tls数据中的Agent地址。
_op:BinaryOp类的对象,上面讲过这个op的作用了。
_global_result:ResultTp类型,保存汇总结果
_result_identity:ResultTp类型的初始值,用于快速reset ResultTp类型变量
element_identity: ElementTp类型的初始值,用于快速reset ElementTp类型变量
agents:存放所有Agent的双向链表。怎么存放的:每个Agent有前向指针previous_和后向指针next,所以把所有的Agent的previous_和next_赋值、串联起来就构成了一个双向链表,我们只要保存这个链表的头,就能依次遍历获得全部的Agent。注意这里并没有改变底层存储Agent的地址,他们实际还是存在AgentGroup中的(多个)一维数组中,只不过通过Agent的previous
、next_串联了起来。

_agents的实现方式是一个很好的借鉴的地方。一方面为了减少内存碎片、提高cpu cache命中率和便于回收再利用,底层存储用多个一维数组连续存储,另一方面其中的一些数据因为某些原因需要组成隐式的“一组”,那么就可以使用双向链表的方式组织在一起。

构造函数

在这里插入图片描述
构造函数中申请一个AgentId。

析构函数

在这里插入图片描述
析构函数中调用了clear_all_agents,并且让AgentGroup回收了AgentId。clear_all_agents的代码:
在这里插入图片描述
clear_all_agents加锁做如下事情:遍历_agents中的每个节点(实际是Agent类型),调用Agent的reset函数(element重置为默认值,combiner重置为NULL),并将该node从双向链表_agents中删除。执行结束后_agents包含0个agent。

函数get_or_create_tls_agent

在这里插入图片描述

一个bvar需要写数据时一定会调用这个函数。它是通过_id快速找到当前线程tls数据中的Agent的地址。AgentGroup::get_tls_agent和AgentGroup::get_or_create_tls_agent之前已经介绍过了,不再赘述。这个过程完全没有锁的,因为操作的都是tls数据。这个是即使再频繁的写bvar数据、性能也非常好的核心所在,因为写时是完全没有数据竞争的。当然了,读的时候会有锁。

306~311行是什么作用?后文会讲解,这里先留意一下。

函数combine_agents

在这里插入图片描述
需要读一个bvar数据时一定会调用这个函数。它的目的是AgentCombiner类的核心作用,即把bvar在所有线程tls中存储的Agent的值“累加”起来。

此函数是线程安全的,即可以在任何线程被同时调用。

当前bvar所有tls的Agent可以通过双向链表_agents查询到。初始化ret为_global_result的值,遍历_agents中的每个节点,将该节点的Agent的实际值临时保存在tls_value,然后通过_op将tls_value“累加”到ret。最后返回ret。

函数reset_all_agents

在这里插入图片描述
此函数是线程安全的,即可以在任何线程被同时调用。

_agents中的每个节点的值进行“累加”,并和_global_result“累加”,保存到tmp最后返回;并且每个节点的值重置为_element_identity(ElementTp类型默认值);

函数commit_and_erase

在这里插入图片描述

commit_and_erase只会被Agent的析构函数调用。获得准备析构的agent的值并“累加”到_global_result,然后从_agent中删除这个agent。

agent什么时候析构进而调用这个commit_and_erase函数?我理解是AgentGroup中的_s_tls_blocks空间不足需要resize的时候可能会因为realloc导致agent复制到新的对象、原对象析构,但因为双向链表_agent中的previous_和next_都是指向agent地址,所以一旦agent地址发生改变,那么_agent就指向了错误的地址,所以需要析构的agent调用commit_and_erase以保证程序正确。所以这里考虑的很细,不这么做的话程序就可能会coredump了。

同时也知道了_global_result的作用,就是把需要迁移的agent的值都保持到_global_result中。

但是就有一个问题,_s_tls_blocks realloc虽然把当前的agent从_agent删除了,但是它原来的值会拷贝到新的对象(我们暂时称为new agent)啊,也就是说new agent的值并不是从初始化值开始的,一旦再次写入,老的值和已经“累加”到_global_result的值——岂不是算了两次。解决方法就是get_or_create_tls_agent函数306~311行,再看下:
在这里插入图片描述
由于agent析构函数中会将combiner重置为NULL,拿到agent之后会判断一下combiner是否会空,不为空则agent是有效的,直接返回;为空则就是我上面说到的情况,那么要调用reset函数,将此agent的值重置_element_identity(ElementTp默认值),并重新加入到双向链表_agents中。

通过上面一些判断和操作以保证:
(1)gentGroup中的_s_tls_blocks resize时双向链表_agents不会指向错误的地址
(2)从_agents中删除的agent值不会被丢掉(_global_result的作用)
(3)agent的值也不会被多计入(get_or_create_tls_agent函数306~311行)

函数commit_and_clear

在这里插入图片描述
获得agent的值并“累加”到_global_result中,然后重置agent中的值。

(待续)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值