先说点啥
在写第一篇CBE的源码博客—— 角色账号登录和管理 时,注意到CBE里面到处充斥着诸如_g_objPool对象,显而易见,那肯定是个对象池,今天闲来无事,准备读一读CBE的对象池是咋实现的,然后就有了这篇小文章。
同时,在读源码时,发现这里面有个叫createObject的泛化接口存在一个隐藏的致命BUG,并做了相关实验验证,具体的会在下面提到。
何为对象池
顾名思义,对象池,就是用来放对象的池子。在面向对象编程里,万物都可以抽象成对象,可以是一个网络处理对象,可以是一个具体的游戏实体对象。在C++中,一个堆对象的出生需要靠new,消亡需要靠delete。在堆上申请空间,创建出一个对象出来,是需要一定的CPU消耗的,如果某一类对象会非常高频的被创建和销毁(比如网络包等),那这部分创建对象和销毁对象的CPU消耗是不能忽视的。
如何解决这个问题呢?可以考虑在一开始就先申请一块足够大的内存空间来存放某类对象实例,当需要创造一个新对象时,不用直接问操作系统,先看看这个预申请的空间中是否还有闲置的对象可以复用,以此避免频繁让操作系统进行开辟/销毁内存空间。这个预申请的空间就是“对象池”。
对象池是应用层面的东西,通过对象复用的方式来避免高频、重复创建对象,它会事先创建一定数量的对象放到池中,当需要创建对象时,直接从对象池中获取,用完对象之后再放回到对象池中,以便复用,如果池子空了,则开次创建一定量的对象投入池子中。
CBE中的对象池
CBE中对象池实现的代码是lib/common/objectpool.h,具体详细的自行打开阅读~
先上一张结构UML图,如下:
通过上图可以一目了然,所有能放进池子里面的对象都必须是PoolObject的子类,PoolObject里面的主要虚函数(需要子类重写的)如下:
- destructorPoolObject:析构前的处理
- onReclaimObject:对象回收时触发
- getPoolObjectBytes:获取对象的实际占位大小
对象池PoolObject的基本成员变量:
template< typename T, typename THREADMUTEX = KBEngine::thread::ThreadMutexNull >
class ObjectPool
{
public:
typedef std::list<T*> OBJECTS;
protected:
OBJECTS objects_;
size_t max_;
bool isDestroyed_;
// 一些原因导致锁还是有必要的
// 例如:dbmgr任务线程中输出log,cellapp中加载navmesh后的线程回调导致的log输出
THREADMUTEX* pMutex_;
std::string name_;
size_t total_allocs_;
// Linux环境中,list.size()使用的是std::distance(begin(), end())方式来获得
// 会对性能有影响,这里我们自己对size做一个记录
size_t obj_count_;
// 最后一次瘦身检查时间
// 如果长达OBJECT_POOL_REDUCING_TIME_OUT大于OBJECT_POOL_INIT_SIZE,则最多瘦身OBJECT_POOL_INIT_SIZE个
uint64 lastReducingCheckTime_;
// 记录的创建位置信息,用于追踪泄露点
std::map<std::string, ObjectPoolLogPoint> logPoints_;
};
代码注释其实写的很详细了,对象池中的所有对象都放在一个名为objects_的对象列表中,obj_count是用来记录当前池子中闲置对象的个数,total_allocs_是指整个池子总共创建的对象个数。
- 池子内存管理
在某些处理高峰期,可能会造成池子的短暂性扩容,之后系统并不需要那么大的池子,对于这种池子,如果不对其进行“瘦身”,其实是白占地土的。CBE里面,对池子瘦身是发生在对一个池对象或者池对象列表的回收流程中。每次回收对象时,会判断当前池子的大小是否大于了初始的大小OBJECT_POOL_INIT_SIZE,如果两次检测时发现大小都超过,并且检测时长大于OBJECT_POOL_REDUCING_TIME_OUT值,则对未使用对象进行回收,以此缩小池子大小。void reclaimObject_(T* obj) { if(obj != NULL) { if(size(