在性能优化的话题中,对象池总是一个会被提及的设计.
那到底什么是对象池?
简单来说就是用完以后别销毁,下次要用的时候重复利用.
那如果不用对象池,是什么样的?
以C++为例.当创建一个对象时,使用new运算符在堆上创建了一个类对象.在用完以后使用delete进行释放内存.因为会涉及到堆内存的申请,效率在某些时候可能会不是那么高.
而且还有另外一个问题.一旦涉及到内存的创建和销毁,就会有内存错误的隐患.比如常见的重复释放,释放错误的内存地址等.
而对象池就正好能够解决上面这些问题.
外部想要创建一个对象,从内存池中申请.用完以后回收到对象池中.
代码大概是这样的
class TestObject : ClassObject
{
public:
void resetProperty() override
{}
}
TestObject* a = mTestObjectPool->newClass();
mTestObjectPool->destroyClass(a);
上面这段代码中的ClassObject是可以放进对象池中的基类.里面放了一些基本的变量.比如对象是否已经回收,对象唯一分配ID,所属对象池.
还有一个resetProperty虚函数.
为什么不写短点叫reset呢?感觉reset命名过于广泛.容易与已有的类函数重名,所以就改成叫resetProperty.冲突几率较小.
这个函数的作用就是重置类对象的成员变量.使其恢复到刚构造完的状态.正是因为有这个函数,才能够使对象池的使用跟new/delete完全一致.但也最容易出错的地方也正是这个函数.常常会发现漏掉重置某个变量.
这里的mTestObjectPool是指定命名空间下的全局变量,算是单例模式的使用.只不过为了简化编写,无需写类名.相比于很多写法,如TestObjectPool::mInstance.至少简单那么一点点.能少写一些是一些.
内存池的实现中.申请创建对象时,会先从一个vector中看看有没有可用的对象,有就返回出去,没有就new一个.
回收对象时就需要特别小心.需要检查是否为空指针,是否为已经回收的对象,是否属于当前内存池.如果是仅主线程用的内存池还要判断当前是否为主线程.都满足以后才会调用重置属性,放入到对象列表中.
以下是完整代码示例
#pragma once
#include "FrameClassDeclare.h"
#include "FrameDefine.h"
class ClassPooledObject
{
BASE(ClassPooledObject, IEventListener);
public:
void markUsable(FrameComponent* classPool, const llong assignID)
{
mCreateSourcePool = classPool;
mAssignID = assignID;
mDestroy = false;
}
bool markDispose(FrameComponent* classPool)
{
if (mCreateSourcePool != classPool || mDestroy)
{
return false;
}
resetProperty();
return true;
}
FrameComponent* getPool() const { return mCreateSourcePool; }
llong getAssignID() const { return mAssignID; }
bool isPendingDestroy() const { return mPendingDestroy; }
bool isDestroy() const { return mDestroy; }
void setPendingDestroy() { mPendingDestroy = true; }
virtual void resetProperty()
{
mCreateSourcePool = nullptr;
mAssignID = 0;
mPendingDestroy = false;
mDestroy = true;
}
protected:
FrameComponent* mCreateSourcePool = nullptr;// 所属的对象池
llong mAssignID = 0; // 被分配时的唯一ID,每次分配都会设置一个新的唯一执行ID
bool mPendingDestroy = false; // 是否已经加入销毁队列,部分类会有延迟销毁的操作,需要标记是否即将被销毁,即将被销毁的对象不应该再执行任何逻辑
bool mDestroy = true; // 当前对象是否已经被回收
};
#pragma once
#include "ClassPoolBase.h"
#include "ClassPooledObject.h"
#include "FrameMySQLUtility.h"
// 固定类型的对象池
// 仅在主线程用
template<typename ClassType, typename TypeCheck = typename IsSubClassOf<ClassPooledObject, ClassType>::mType>
class ClassPool : public ClassPoolBase
{
BASE(ClassPool, ClassPoolBase);
public:
void initDefault(const int count)
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
Vector<ClassType*> list(count);
FOR_I(count)
{
ClassType* obj = new ClassType();
obj->resetProperty();
list.push_back(obj);
}
mUnusedList.addRange(list);
mTotalCount += count;
}
void quit() override
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
for (ClassType* obj : mUnusedList)
{
delete obj;
}
mUnusedList.clear();
}
ClassType* newClass()
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return nullptr;
}
ClassType* obj = nullptr;
if (mUnusedList.size() > 0)
{
// 首先从未使用的列表中获取,获取不到再重新创建一个
obj = mUnusedList.popBack(nullptr);
}
// 没有找到可以用的,则创建一个
if (obj == nullptr)
{
obj = new ClassType();
obj->resetProperty();
if (++mTotalCount % 5000 == 0 && mShowCountLog)
{
LOG(string(typeid(*obj).name()) + "的数量已经达到了" + IToS(mTotalCount) + "个");
}
}
// 设置为可用状态
obj->markUsable(this, ++mAssignIDSeed);
return obj;
}
void destroyClass(ClassType* obj)
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
// 如果当前对象池已经被销毁,则不能再重复销毁任何对象
if (mDestroied || obj == nullptr)
{
return;
}
if (!obj->markDispose(this))
{
ERROR_PROFILE((string("0重复销毁对象:") + typeid(ClassType).name()).c_str());
return;
}
// 添加到未使用列表中
mUnusedList.push_back(obj);
}
void destroyClassList(const Vector<ClassType*>& objList)
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
// 如果当前对象池已经被销毁,则不能再重复销毁任何对象
if (mDestroied || objList.size() == 0)
{
return;
}
// 再添加到列表
mUnusedList.reserve(mUnusedList.size() + objList.size());
for (ClassType* obj : objList)
{
if (obj == nullptr)
{
continue;
}
if (!obj->markDispose(this))
{
ERROR_PROFILE((string("1重复销毁对象:") + typeid(ClassType).name()).c_str());
continue;
}
mUnusedList.push_back(obj);
}
}
template<int Length>
void destroyClassList(const ArrayList<Length, ClassType*>& objList)
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
// 如果当前对象池已经被销毁,则不能再重复销毁任何对象
if (mDestroied || objList.size() == 0)
{
return;
}
// 再添加到列表
mUnusedList.reserve(mUnusedList.size() + objList.size());
FOR_I(objList.size())
{
ClassType* obj = objList[i];
if (obj == nullptr)
{
continue;
}
if (!obj->markDispose(this))
{
ERROR_PROFILE((string("1重复销毁对象:") + typeid(ClassType).name()).c_str());
continue;
}
mUnusedList.push_back(obj);
}
}
template<typename T0>
void destroyClassList(const HashMap<T0, ClassType*>& objMap)
{
if (!isMainThread())
{
ERROR(string("只能在主线程调用,type:") + typeid(ClassType).name());
return;
}
// 如果当前对象池已经被销毁,则不能再重复销毁任何对象
if (mDestroied || objMap.size() == 0)
{
return;
}
// 添加到未使用列表中
mUnusedList.reserve(mUnusedList.size() + objMap.size());
for (const auto& objPair : objMap)
{
ClassType* obj = objPair.second;
if (obj == nullptr)
{
continue;
}
if (!obj->markDispose(this))
{
ERROR_PROFILE((string("2重复销毁对象:") + typeid(ClassType).name()).c_str());
continue;
}
mUnusedList.push_back(obj);
}
}
protected:
void onHour() override
{
if (mTotalCount > 1000)
{
LOG("ClassPool: " + string(typeid(ClassType).name()) + "的数量:" + IToS(mTotalCount));
}
}
protected:
Vector<ClassType*> mUnusedList; // 未使用列表
llong mAssignIDSeed = 0; // 对象的分配ID种子
int mTotalCount = 0; // 创建的对象总数
bool mShowCountLog = true; // 当对象池创建总数达到一定数量时,是否打印日志信息,一般打印,但是日志的对象池不能打印
};
更多的扩展
1.内存池中的辅助逻辑
可以看到,上面的代码中还有一些其他的逻辑.
比如每小时输出一次当前内存池总共的对象数量.当累计创建数量每次超过一定值时就输出一次信息.用于辅助调试内存分配情况.
还有添加了批量创建,批量销毁,以便尽量将循环延迟到内存池内,而不是在外边.
2.类型映射的内存池
还有其他的扩展,比如上面的代码是固定类型内存池.所以使用的时候就是这样,定义一个Account的内存池.只能创建Account的类对象.
#pragma once
#include "ClassPool.h"
#include "Account.h"
class AccountPool : public ClassPool<Account>
{};
实际代码中还会有其他的需求,比如根据指定的Key来映射指定的类型.比如定义了一个枚举
enum class TEST_ENUM : byte
{
OBJECT_A,
OBJECT_B,
OBJECT_C,
}
三个枚举对应三个类型,这时候就要搭配工厂模式来创建对应的对象.
3.多线程内存池
子线程中有时候也需要创建对象.所以如果直接将上面的内存池添加一套多线程同步代码,会影响仅主线程创建对象的效率.所以最好的办法是分开.主线程的完全不需要加锁同步,原子操作,减少很多不必要的逻辑.而子线程是为了保证多线程同步,避免崩溃,需要添加必要的锁.
当然,如果可以保证只在一个线程去访问,也完全可以不加锁.
4.如何保证resetProperty中已经完全重置了成员变量
这是一个比较麻烦的问题.最有效的方案还是使用静态代码检查.
自己写一个工具.能够解析每一个可回收类的所有成员变量.并且找到resetProperty的函数定义,然后跟resetProperty中的进行对比,查看有哪些变量被遗漏了.