前面介绍了agent,本篇我们来聊聊combiner,这也是bvar的一个特别重要的基础组件,整个combiner相关的都在combiner.h源文件里,由globalvalue、elementcontainer、agentcombiner三大类构成,agentcombiner里还有个很重要的子struct agent,这也是上次介绍过的agentgroup里保存的数据类型。下面就分别介绍下这几个类的作用和实现。
globalvalue,全局变量辅助类,保存了agent和combiner的指针,主要是提供了一些同步机制,用于把agent的值往combiner的全局结果里合并。
正如字面含义,elementcontainer是实际元素的容器,也就是对实际类型的封装,之所以需要这么一个封装是因为需要保证多线程操作情况下的同步正确,bvar在本地的tls存储除了会被当前线程使用,还会在聚合读取等场景下被其他线程使用,elementcontainer是一个模板类,而且进行了特化,通用模板定义如下:
可以看到,提供了读、写、交换、编辑(根据传入的op和value修改本地value),并且使用了锁来保证同步。模板参数有两个,分别是元素类型t和使能器enabler,这个enabler比较有意思,也是特化版本所用到的重要部分。
除了这个通用版本,还有一个针对原子类型变量的特化版本,加锁的开销相对是比较大的,对于int float等本身就带原子性的类型,通过原子操作就能保证同步,bvar作为一个重视性能的计数器,自然不会放过这种优化点,于是就有了下面这种特化:
这里将第二个模板参数进行了特化,使用了typename butil::enable_if
这个特化模板里都是通过原子操作来实现各种操作的,由于不需要额外的数据同步,因此所有的memory_order都是relaxed。
这两个模板都实现了load store exchange和modify,通用模板里有一个独有函数merge,而原子变量模板有一个compare_exchange_weak是独有的,这和实际用途有关系。
agentcombiner也是整个combine的主体,是整个combiner最核心的部分
3.1 定义
agentcombiner基本定义如下:
agentcombiner同样是一个模板,模板参数为结果类型,元素类型和二元操作符, resulttp和elementtp分别用于聚合结果和单个tls元素,对于adder这种普通reducer类型的bvar,二者是完全一样的:
对于其他复杂一点的类型,比如percentile,这二者是有区别的:
三个typedef是为了简化后面的一些定义,申明globalvalue
3.2 内部结构体agent
下面我们先看下agentcombiner一个重要的子结构体:
这也是实际存储在agentgroup里的类型,刚开始看到struct agent : public butil::linknode这种写法的时候有点懵逼,学习了下才知道原来这是奇异递归模板模式(curiously recurring template pattern),简称crtp,这里这么使用的好处是可以提高运行效率,具体的可以找相关资料学习,这里不多说。
butil::linknode是一种双向链表节点类型,提供了向前取向后取向前插入向后插入取值赋值等等基本操作,这里agent通过继承的方式基于链表来进行agent的数据的保存和处理,
combiner指针指向当前agent所属bvar的combiner,初始值为null,可以据此判断此agent是否已经分配。element为实际保存数据的变量。
提供了reset函数对combiner和 element进行重置,merge_global函数用于将当前agent里的tls值merge到combiner的global result里,部分bvar类型需要用到,比如percentile。
析构函数里调用combiner的commit_and_erase来提交现有数据并去除该agent(从combiber维护的agent链表里移除)。
3.3 成员变量
成员变量如下:
(1)_id:由agentgroup分配的对应当前bvar变量的一个id,用于去tls block里寻址.
(2)_op:此bvar的操作符。
(3)_lock:用于操作全局汇总结果的锁。
(4)_global_result:用来保存汇总结果。
(5)_result_identity和_element_identity:比较特殊,本质上分别是resulttp和elementtp类型的初始化过后不经其他修改的变量,被各种reset性质的函数用来清空变量。
(6)_agents保存了当前bvar所有agent的链表。
3.3 构造函数和析构函数
(1)构造函数:
给各个成员变量赋值,_id的赋值调用的是agentgroup的create_new_agent()函数,分配了一个当前bvar独有的id。
(2)析构函数:
清除agent数据然后归还id。
3.3 主要功能函数
(1)combine_agents()
汇聚所有agent的值返回,这也是外部最常调用的一个函数,比如reducer的get_value调用的就是这个函数,遍历agent链表汇总所有agent的tls值和_global_result保存的值返回。
(2)reset_all_agents()
重置所有的agent,包括combiner内部的_global_result,返回汇总值,和combine_agents很类似,区别就是会额外重置agent值和 _global_result值。
(3)clear_all_agents()
清除所有agents,combiner的析构函数会调用次函数,因为agent有可能被重用,所以对每个agent都要reset一下
(4)commit_and_erase(agent *agent)
提交tls值并且移除agent,agent的析构函数会调用这个,也就是某个线程退出的时候会调用这个函数实现提交完数据后将自己的agent从bvar的agent list里移除。
(5)commit_and_clear
提交tls值并且利用_element_identity将tls置为初值,有些bvar需要用到这个功能。
(6)get_or_create_tls_agent()
这个函数前面的文章提到过,获取或者创建本线程对应的agent,因为需要尽可能快,所以首先调用更快的agentgroup::get_tls_agent,如果是null才去调用agentgroup::get_or_create_tls_agent,由于只有线程第一次访问没有agent的时候才会是null,所以大部分情况都能比较快地得到agent,中间判断如果agent->combiner非null,说明是先前建立的agent,直接返回即可,否则需要把当前combiner赋给它并且将agent挂到_agents链表里面。
在bvar里,combiner的角色无疑是非常重要的,它负责协调各个agent,提供汇总等针对各个agent的操作,直接和agentgroup交互,给各个特定线程提供agent。同时根据源码也能看出为了追求高性能确实下了很多功夫。