2021SC@SDUSC
一、AgentCombiner类简介
上篇文章我们介绍了AgentGroup类,它主要负责agent的创建和管理,按块存储agent。这篇文章叙述的AgentCombiner则是针对agent的操作,它负责协调各个agent,提供存储、合并、重置等针对各个agent的操作,直接和AgentGroup交互,给各个特定线程提供agent。下文将分析./bvar/detail/combiner.h中的代码,包括了AgentCombiner类和它用到的相关类的内容。
二、代码分析
首先是GlobalValue类,我们也可以通过注释理解到,它的设计是为了避免后面的AgentCombiner::reset()方法可能造成的死锁,所以利用提供的Agent和Combiner编写了特制的lock和unlock锁机制。
然后还有一个元素容器类,因为在不同的TLS中,元素类型不同,所以写出一个统一的模板类。而且为了保证多线程下的同步问题,所以要重构这些简单的操作,就是加锁保证操作原子化。
封装的原子化操作有(其本质是封装了一个元素,对这个元素做操作):
load:将该值载入TLS。
store:修改该值。
exchange:将该值与TLS的某个值交换。
modify:将该值与TLS某一值进行op操作。
merge_global:将该值与GlobalValue进行op操作。
还有一个针对原子变量的版本,ElementContainer的第二个变量是判断了T是否是原子类型的变量的方法,如果是ElementContainer则会匹配到这个类中,这个类是上一个类的特化版本。加锁的性能相对是比较差的,对于int float数据类型本身就有原子性,通过原子操作就能保证同步,所以在这些数据下,用这个特化版本不加锁可以提升效率。
看完这两个辅助类,我们进入combiner.h最重要的部分——AgentCombiner类。
首先解释下属性的用途。
首先由三个模板参数。 ResultTp和ElementTp分别表示聚合的结果和单个tls元素类型,BinaryOp是操作符。GlobalValue<self_type>的友元类是因为GlobalValue后续要获得combiner的私有变量。
对于之前讲过的几种常用的普通Reducer类型的bvar,ResultTp和ElementTp是完全一样的。
然后是之前也屡次提到的Agent类,这也是Combiner的核心部分,也可以说是整个TLS存储全流程的核心类。
开头的struct Agent : public butil::LinkNode<Agent>是一个很有意思的c++语法。它是奇异递归模板模式 (CRTP, Curiously Recurring Template Pattern),将派生类作为基类的模板参数,意味基类可以访问派生类的一些内容,这也是名字中为什么会有递归两个字的原因。通过CRTP可以使得类具有类似于虚函数的效果,同时又没有虚函数调用时的开销(虚函数调用需要通过虚函数指针查找虚函数表进行调用),同时类的对象的体积相比使用虚函数也会减少(不需要存储虚函数指针),但是缺点是无法动态绑定。
butil::LinkNode是一种双向链表节点类型,有类似deque双向队列的一系列处理方法,这里Agent类通过继承的方式基于链表来进行数据的管理。
combiner指针指向当前agent所属bvar的combiner,初始值为Null,可以据此判断此agent是否已经分配。
element为实际保存数据的变量。
有两个成员函数。reset函数对combiner和 element进行重置。merge_global函数用于将当前Agent里的TLS值进行合并操作到Combiner的_global_result里。
析构函数里调用combiner的commit_and_erase来提交现有数据并去除该Agent(从Combiner维护的Agent链表里移除)。
接下来看私有变量,除去之前介绍过的三个模板变量,其他的稍作解释。
_id:由AgentGroup分配的对应当前bvar变量的一个id,用于去寻址匹配。
_lock:解决同步问题的锁。
_global_result:用来保存合并的结果。
_agents:保存了当前bvar所有Agent的链表。
看完属性来看方法。构造函数的_id是调用我们上篇文章讲到的方法分配的id,析构函数清空值并且根据这个_id调用AgentGroup的销毁方法。
combine_agents合并所有Agent的值返回,这也是外部最常调用的一个函数,比如之前见到的get_value调用的就是这个函数,操作不复杂,就是遍历_agents链表合并所有Agent的tls_value值,结果返回合并值。
reset_all_agents重置所有Agent的值返回,这同样是外部最常调用的一个函数,和combine_agents很类似,结果同样返回合并值,只是在同样的遍历链表操作中重置了Agent值和 _global_result值。
存储了TLS值并且从列表中移除Agent。上面的析构函数调用过该函数,也就是某个线程退出的时候会调用这个函数实现提交数据和删除。
提交tls值并且利用_element_identity将TLS重置,和erase的删除不同,Agent被保留重置,有些bvar需要用到这个功能。
这是上文分析过的两个根据id获取Agent的封装,获取或者创建本线程对应的Agent,为了提高效率,首先调用更快的AgentGroup::get_tls_agent,如果是null才去调用AgentGroup::get_or_create_tls_agent,由于只有线程第一次访问没有Agent的时候才会是null,所以大部分情况都能比较快地得到agent,中间判断如果agent->combiner非null,说明是先前建立的agent,直接返回即可,否则需要把当前combiner赋给它并且将agent挂到_agents链表里面。
清除所有agents,析构时调用过,注释也提到因为Agent有可能被重用,所以对全部的Agent都要重置。