对象池模式(摘录)
原文1: 对象池模式 C++实现.
原文2: C++对象池的实现和原理.
对象池模式
对象池是一种对象复用技术。Mark Grand在《Patterns in Java》描述了对象池的设计模式,它通过管理和复用有限对象来共享某些稀少或必须付出昂贵代价的资源,该模式的UML结构分为3部分。
Reusable:被复用的对象或资源。
Client:复用对象的使用者,1个Client对象可以使用多个复用对象,一个复用对象只能同时被1个Client
使用。
ReusablePool:对象池即复用对象的容器或集合,用来管理和存储可复用的对象。一般来说为保证对象复用
的安全,对象池将按照Singleton的模式设计为全局唯一实例。
该模式被广泛使用,出现了连接池、线程池等许多应用。
理解对象池模式
对象池模式管理一个可替代对象的集合。组件从池中借走对象,用它来完成一些任务并当任务完成时归还该对象。被归还的对象接着满足请求,不管是同一个组件还是其他组件的请求。对象池模式可以管理那些代表现实资源或者通过重用来分摊昂贵初始化代价的对象。
使用对象池
对象池技术可以避免在程序的生命周期中创建和删除大量对象。如果知道程序需要同一类型的大量对象,而且对象的生命期都很短,就可以为这些对象创建一个池(pool)进行缓存。只要代码中需要一个对象,就可以向对象池请求。用完此对象时,要把它放回到池中。对象池只创建一次对象,因此他们的构造函数只调用一次,而不是每次使用都调用。因此,当构造函数要完成一些设置动作,而且这些设置可以应用于该对象的多次使用时,对象池就很合适。另外在非构造函数方法调用中要在对象上设置特定于实例的参数时,也很适用于对象池。
一个对象池实现
对象池类模板的实现。这个池在构造时会分配一大块(chunk)指定类的对象(块可理解为包括许多对象,即一堆对象),并通过acquireObject()方法交出对象。当客户用完这个对象时,会通过releaseObject()方法将其返回。如果调用了acquireObject,但是没有空闲的对象,池会分配另一块对象。
对象池实现中最难的一方面是要记录那些对象是空闲的,那些对象正在使用,这个实现采用了以下做法,即把空闲的对象保存在一个队列中。每次客户请求一个对象时,池就会把队列中的第一个对象交给该客户。这个池不会显式地跟踪正在使用的对象。它相信客户在用完对象后会正确地将对象交还到池中。另外,这个池会在一个像两种记录所有已分配的对象。这个向量尽在撤销池时才会用到,以便释放所有对象的内存,从而避免内存泄漏。
代码示例:
#include <queue>
#include <vector>
#include <memory>
#include <stdexcept>
#include <algorithm>
using namespace std;
//#define kDefaultChunkSize 10
template <typename T>
class ObjectPool
{
public:
ObjectPool(int chunkSize = kDefaultChunkSize)
throw(invalid_argument, bad_alloc);
~ObjectPool();
T& acquireObject();
void releaseObject(T& obj);
protected:
//m_FreeList stores the objects that are not currently in use by clients
queue<T*> m_FreeList;
//m_AllObjects stores pointers to all the objects, in use or not.
//This vector is needed in order to ensure that all objects are freed properly in the destructor
vector<T*> m_AllObjects;
int m_ChunkSize;
static const int kDefaultChunkSize = 10;
//Allocates m_ChunkSize new objects and adds them to the m_FreeList
void allocateChunk();
static void arrayDeleteObject(T* obj);
private:
ObjectPool(const ObjectPool<T>& src);
ObjectPool<T>& operator=(const ObjectPool<T>& rhs);
};
template <typename T>
const int ObjectPool<T>::kDefaultChunkSize;
template <typename T>
ObjectPool<T>::ObjectPool(int chunkSize)
throw(invalid_argument, bad_alloc) : m_ChunkSize(chunkSize)
{
if (m_ChunkSize <= 0)
{
throw invalid_argument("chunk size must be positive");
}
//create m_ChunkSize objects to start.
allocateChunk();
}
//在连续的存储空间中分配m_ChunkSize个元素,
//它会在vector中存储对象数组的指针,并把每个对象压至queue中。
template <typename T>
void ObjectPool<T>::allocateChunk()
{
T* newObjects = new T[m_ChunkSize];
m_AllObjects.push_back(newObjects);
for (int i = 0; i < m_ChunkSize; i++)
{
m_FreeList.push(&newObjects[i]);
}
}
//Freeing function for use in the for_each algorithm in the destructor
template <typename T>
void ObjectPool<T>::arrayDeleteObject(T* obj)
{
delete [] obj;
}
template <typename T>
ObjectPool<T>::~ObjectPool()
{
for_each(m_AllObjects.begin(), m_AllObjects.end(), arrayDeleteObject);
}
//返回空闲列表中的队头对象,如果没有空闲对象则首先调用allocateChunk
template <typename T>
T& ObjectPool<T>::acquireObject()
{
if (m_FreeList.empty())
{
allocateChunk();
}
T* obj = m_FreeList.front();
m_FreeList.pop();
return (*obj);
}
//将对象返回到空闲列表的队尾
template <typename T>
void ObjectPool<T>::releaseObject(T& obj)
{
m_FreeList.push(&obj);
}
使用对象池
考虑一个要从用户得到请求并处理这些请求的应用。这个应用很可能是图形前端和后端数据库之间的一个中间件。可以把每个用户请求编码到一个对象中。
//使用对象池,用户请求
class UserRequest
{
public:
UserRequest() {}
~UserRequest() {}
//Methods to populate the request with specific information
//Methods to retrive the request data
private:
//data members
};
//客户
class Client
{
public:
Client() {}
~Client() {}
UserRequest& obtainUserRequest(ObjectPool<UserRequest>& pool);
void processUserRequest(ObjectPool<UserRequest>& pool, UserRequest& req);
private:
};
UserRequest& Client::obtainUserRequest(ObjectPool<UserRequest>& pool)
{
cout << "Client::obtainUserRequest: " << pool.getCount() << endl;
UserRequest& request = pool.acquireObject();
sleep(10);
//Populate the request with user input
return request;
}
void Client::processUserRequest(ObjectPool<UserRequest>& pool, UserRequest& req)
{
//Process the request
cout << "Client::processUserRequest: " << pool.getCount() << endl;
pool.releaseObject(req);
}
int main()
{
ObjectPool<UserRequest> requestPool(1000);
while (/*program is running*/)
{
UserRequest& req = obtainUserRequest(requestPool);
processUserRequest(requestPool, req);
}
}
C++对象池的实现和原理
什么是对象池
对象池是一种空间换时间的技术,对象被预先创建并初始化后放入对象池中,对象提供者就能利用已有的对象来处理请求,并在不需要时归还给池子而非直接销毁它减少对象频繁创建所占用的内存空间和初始化时间
对象池原理
描述一个对象池有两个很重要的参数,一个是这个对象池的类型,另一个是这个对象池可以获得对象的数量对象池的实现和内存池的实现原理很像:都是一开始申请大内存空间,然后把大内存分配成小内存空间,当需要使用的时候直接分配使用,不在向系统申请内存空间,也不直接释放内存空间。使用完之后都是放回池子里不同的地方在内存池有一个映射数组,在使用时负责快速定位合适的内存池(一个内存池可以有很多内存块大小不同的池子)但是每一个类型的对象只对应一个对象池,并自己管理自己的对象池。不同类型的对象池是相互独立的存在
对象池的优点
1、减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用
2、提高了获取对象的响应速度,对实时性要求较高的程序有很大帮助
3、一定程度上减少了垃圾回收机制(GC)的压力
对象池的缺点
1、很难设定对象池的大小,如果太小则不起作用,过大又会占用内存资源过高
2、并发环境中, 多个线程可能(同时)需要获取池中对象, 进而需要在堆数据结构上进行同步或者
因为锁竞争而产生阻塞, 这种开销要比创建销毁对象的开销高数百倍;
3、由于池中对象的数量有限, 势必成为一个可伸缩性瓶颈;
4、所谓的脏对象就是指的是当对象被放回对象池后,还保留着刚刚被客户端调用时生成的数据。
脏对象可能带来两个问题
4.1 脏对象持有上次使用的引用,导致内存泄漏等问题。
4.2 脏对象如果下一次使用时没有做清理,可能影响程序的处理数据。
什么条件下使用对象池
1、资源受限的, 不需要可伸缩性的环境(cpu\内存等物理资源有限): cpu性能不够强劲, 内存比较紧张,
垃圾收集, 内存抖动会造成比较大的影响, 需要提高内存管理效率,响应性比吞吐量更为重要;
2、数量受限的, 比如数据库连接;
3、创建对象的成本比较大,并且创建比较频繁。比如线程的创建代价比较大,于是就有了常用的线程池。
对象池实现过程
对象池的实现代码分三部分,一个是对象池的内部实现类(ObjectPool),另一个是管理对象池的类(ObjectPoolBase)
对象池内部的实现和内存池基本一样,都是先申请一大块内存空间,然后再把大内存分成许多小内存,每个小内存对应一个对象,这些小内存以链表的形式进行管理
内存池的管理主要是对用户提供接口,参数的传递通过可变参的模板类来实现,对new和delete操作进行封装是为了更方便的初始化对象,让使用时更加灵活;要创建不同类型的对象池,只需要继承ObjectPoolBase这个类就可以,当发现该类型对应的对象池没有初始化的时候,就会创建该对象池并且进行初始化操作,之后就可以正常分配对象
注意:
对象池和内存池可以一起使用,在一起使用的情况下,如果要在对象池中使用智能指针,应该特别注意new操作,因为智能指针使用的是全局的new,内存池使用的是类里面的new,在申请内存时会有差别
对象池文件
#ifndef _ObjectPoolBase_hpp_
#define _ObjectPoolBase_hpp_
#include<stdlib.h>
#include<assert.h>
#include<mutex>
#ifdef _DEBUG
#ifndef xPrintf
#include<stdio.h>
#define xPrintf(...) printf(__VA_ARGS__)
#endif
#else
#ifndef xPrintf
#define xPrintf(...)
#endif
#endif // _DEBUG
//模板给对象池提供参数接口
template<class Type, size_t nPoolSzie>
//对象池的实现
class CELLObjectPool
{
public:
CELLObjectPool()
{
initPool();
}
~CELLObjectPool()
{
if (_pBuf)
delete[] _pBuf;
}
private:
//NodeHeader是每个对象的描述信息
class NodeHeader
{
public:
//下一块位置
NodeHeader* pNext;
//内存块编号
int nID;
//引用次数
char nRef;
//是否在内存池中
bool bPool;
private:
//预留
char c1;
char c2;
};
public:
//释放对象内存
void freeObjMemory(void* pMem)
{
//首地址往前偏移,对象和对象描述信息的内存一起释放
NodeHeader* pBlock = (NodeHeader*)((char*)pMem - sizeof(NodeHeader));
xPrintf("freeObjMemory: %llx, id=%d\n", pBlock, pBlock->nID);
assert(1 == pBlock->nRef);
//内存在对象池的部分释放之后,指针回到第一个未分配的对象
if (pBlock->bPool)
{
std::lock_guard<std::mutex> lg(_mutex);
if (--pBlock->nRef != 0)
{
return;
}
pBlock->pNext = _pHeader;
_pHeader = pBlock;
}
else {
if (--pBlock->nRef != 0)
{
return;
}
//不在对象池的直接释放内存
delete[] pBlock;
}
}
//申请对象内存,优先向内存池申请内存,内存池不够在向系统申请内存
void* allocObjMemory(size_t nSize)
{
std::lock_guard<std::mutex> lg(_mutex);
NodeHeader* pReturn = nullptr;
//如果对象池已经满了,数量达到上限,需要向系统申请内存
if (nullptr == _pHeader)
{
//计算对象池大小=对象数量*(一个对象内存大小+对象描述信息内存大小)
pReturn = (NodeHeader*)new char[sizeof(Type) + sizeof(NodeHeader)];
pReturn->bPool = false;
pReturn->nID = -1;
pReturn->nRef = 1;
pReturn->pNext = nullptr;
}
else {
pReturn = _pHeader;
_pHeader = _pHeader->pNext;
assert(0 == pReturn->nRef);
pReturn->nRef = 1;
}
xPrintf("allocObjMemory: %llx, id=%d, size=%d\n", pReturn, pReturn->nID,
nSize);
return ((char*)pReturn + sizeof(NodeHeader));
}
private:
//初始化对象池
void initPool()
{
//断言
assert(nullptr == _pBuf);
//对象池不为空(已经初始化过),不初始化直接返回
if (_pBuf)
return;
//计算对象池大小=对象数量*(一个对象内存大小+对象描述信息内存大小)
size_t realSzie = sizeof(Type) + sizeof(NodeHeader);
size_t n = nPoolSzie * realSzie;
//申请池的内存
_pBuf = new char[n];
//初始化内存池
_pHeader = (NodeHeader*)_pBuf;
_pHeader->bPool = true;
_pHeader->nID = 0;
_pHeader->nRef = 0;
_pHeader->pNext = nullptr;
//遍历内存块进行初始化
NodeHeader* pTemp1 = _pHeader;
for (size_t n = 1; n < nPoolSzie; n++)
{
NodeHeader* pTemp2 = (NodeHeader*)(_pBuf + (n* realSzie));
pTemp2->bPool = true;
pTemp2->nID = n;
pTemp2->nRef = 0;
pTemp2->pNext = nullptr;
pTemp1->pNext = pTemp2;
pTemp1 = pTemp2;
}
}
private:
//描述信息块地址
NodeHeader* _pHeader;
//对象池内存缓存区地址
char* _pBuf;
//多线程使用需要加锁
std::mutex _mutex;
};
//模板类给主函数使用对象池提供接口
template<class Type, size_t nPoolSzie>
//使用对象池的类
class ObjectPoolBase
{
public:
//重载给对象申请内存的new操作
void* operator new(size_t nSize)
{
return objectPool().allocObjMemory(nSize);
}
//重载给对象释放内存的delete操作
void operator delete(void* p)
{
objectPool().freeObjMemory(p);
}
//创建对象,用模板类提供对外的可变参数的接口
//这里的模板为啥要用可变参数?
//这里创建一个对象的时候还可能带有初始化的参数,这个参数的个数是不一样的,所有模板的参数需
//要变参
//
template<typename ...Args>
static Type* createObject(Args ... args)
{ //不定参数 可变参数
Type* obj = new Type(args...);
//可以做点其它的事情,比如说对象的放逐和驱逐
return obj;
}
//销毁对象
static void destroyObject(Type* obj)
{
delete obj;
}
private:
//定义不同类型的对象池
typedef CELLObjectPool<Type, nPoolSzie> ClassTypePool;
//单例-饿汉模式,实例化一个对象池
static ClassTypePool& objectPool()
{ //静态CELLObjectPool对象
static ClassTypePool sPool;
return sPool;
}
};
#endif // !_ObjectPoolBase_hpp_
测试文件
#include<stdlib.h>
#include<iostream>
#include<thread>
#include<mutex>//锁
#include<memory>
#include"ObjectPoolBase.hpp"
using namespace std;
//10是池内对象的最大数量,通过继承ObjectPoolBase来使用对象池
class ClassA : public ObjectPoolBase<ClassA, 10>
{
public:
ClassA(int n)
{
num = n;
printf("ClassA\n");
}
~ClassA()
{
printf("~ClassA\n");
}
public:
int num = 0;
};
class ClassB : public ObjectPoolBase<ClassB, 10>
{
public:
ClassB(int n, int m)
{
num = n * m;
printf("ClassB\n");
}
~ClassB()
{
printf("~ClassB\n");
}
public:
int num = 0;
};
int main()
{
{
//对象池的使用过程是在使用new的时候,因为你要创建的类继承了对象池管理的类,
//对象池管理类会根据你设计的对象数量创建(初始化)对象池,
//进入对象池的时候首先判断该对象池是否初始化,初始化过说明该对象池已经存在
//未初始化说明还没有该类型的对象池
//如果该对象池没有初始化,就进行初始化(创建一个对象池)
//已经初始化就直接从该对象池中分配一个对象
//然后通过new操作在对象池中分配一个已经创建好的对象
//再根据对象传进来的参数,确定对象池允许有几个对象
//然后申请相应大小的内存,逐个以链表的形式进行初始化
//两种创建对象的方法
//直接用new创建对象,把new和delete暴露在外,使用不灵活,初始化对象不方便
ClassA* a1 = new ClassA(5);
//delete a1;
//把new和delete封装成一个函数,可以更灵活的对对象进行操作,比如在创建的时候初始化对象
ClassA* a2 = ClassA::createObject(6);
//ClassA::destroyObject(a2);
delete a1;
ClassA::destroyObject(a2);
ClassB* b1 = new ClassB(5, 6);
delete b1;
ClassB* b2 = ClassB::createObject(5, 6);
ClassB::destroyObject(b2);
}
system("pause");
return 0;
}