在我日常的项目中,对象池是使用场景非常广泛的一种手段。本文就对对象池的设计原理进行讲解,并对其底层进行抽象,提供一个可以快速、简单上手的对象池模板类。
对象池原理
对象池是一种空间换时间的技术,对象被预先创建并初始化后放入对象池中,对象提供者就能利用已有的对象来处理请求,并在不需要时归还给池子而非直接销毁。它减少对象频繁创建所占用的内存、空间和初始化时间。
描述一个对象池有两个很重要的参数,一个是这个对象池的类型,另一个是这个对象池可以获得对象的数量。
对象池的实现和内存池的实现原理很像:都是一开始申请大内存空间,然后把大内存分配成小内存空间,当需要使用的时候直接分配使用,不在向系统申请内存空间,也不直接释放内存空间。使用完之后都是放回池子里。
不同的地方在内存池有一个映射数组,在使用时负责快速定位合适的内存池(一个内存池可以有很多内存块大小不同的池子)。而每一个类型的对象只对应一个对象池,并自己管理自己的对象池。不同类型的对象池是相互独立的存在。
对象池的优点:
- 减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用;
- 提高了获取对象的响应速度,对实时性要求较高的程序有很大帮助;
- 一定程度上减少了垃圾回收机制(GC)的压力。
对象池的缺点:
- 很难设定对象池的大小,如果太小则不起作用,过大又会占用内存资源过高;
- 并发环境中,多个线程可能(同时)需要获取池中对象,进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞,这种开销要比创建销毁对象的开销高数百倍;
- 由于池中对象的数量有限,势必成为一个可伸缩性瓶颈。
什么条件下,适合使用对象池?
- 资源受限的,不需要可伸缩性的环境(cpu\内存等物理资源有限):cpu性能不够强劲,内存比较紧张,垃圾收集,内存抖动会造成比较大的影响,需要提高内存管理效率,响应性比吞吐量更为重要。
- 数量受限的,比如数据库连接。
- 创建对象的成本比较大,并且创建比较频繁。比如线程的创建代价比较大,于是就有了常用的线程池。
对象池设计
本文将设计两种类型的对象池:全局对象池和局部对象池。所谓全局对象池,就是指利用单例的方式,确保某种类型的对象池,全局情况下共享一个;而局部对象池,则可以生成多个同种类型的对象池,然后可以在不同的局部进行使用。
- MsgPool 是全局的对象池
- MsgPoolEx 是局部的对象池
按照对象池的原理,最根本的,首先需要提供数据成员用来存储所有的预分配的对象。同时,我们还需要对每个对象是否被使用/是否空闲,进行管理。代码上:
std::vector<T *> items_;
std::list<T *> free_list_;
std::list<T *> used_list_;
为了保证对象池在多线程环境下的安全,对于free_list_和used_list_的使用,必须要用锁进行管理。由于可能涉及到多线程同步,顺便加上条件变量:
// ensure multi-threads safety
mutable std::mutex pool_mutex_;
// ensure multi-thread synchronization
std::condition_variable item_available_cond_;
当我们主动从对象池中获取到一个成员后,对free_list_和used_list_进行调整。这通常是一个主动的过程。然而,当这个成员使用完成后,需要被释放。这也涉及到free_list_和used_list_的调整。但是,这个调整却不是一个主动的过程。而是被动的过程。除此以外,释放完成后,我们还需要对该已经使用过的对象进行清空操作,以保证下次再主动获取到的成员是一个初始化的、干净的成员。
一个非常好的解决思路是:借助智能指针shared_ptr。
默认情况下,shared_ptr可以在引用计数为0的情况下,自动析构。但是如果用户自己指定删除器的话,则会在引用计数为0的情况下,自动调用删除器函数。
那么,我们就可以在删除器中进行free_list_和used_list_的调整操作,并且完成对象的清空操作。假设用户的清空操作为Reset()函数提供。也就是说,用户必须为对象池的对象提供Reset()函数用于调用,否则则会发生crash行为。
关于shared_ptr的使用,不太熟悉的同学可以参考博文:【C++】shared_ptr共享型智能指针详解。
代码实现
假设我们对象池中存放的对象Object的定义如下:
class Object {public:Object(int id = -1) :id_(id) {}~Object() {}void Reset() {id_ = -1;std::cout << "object " << this << " Reset" << std::endl;}void print() {std::cout << this << " id_ : " << id_ << std::endl;}int id_;
};
可以看到,该对象Object提供了Reset()方法。
全局对象池
全局对象池由于使用了单例,因此增加了以下两个数据成员:
static std::mutex singleton_mutex_;
static std::unique_ptr<MsgPool<T> > pool_ptr_;
此时对于某种类型的对象池,全局仅有一个,即pool_ptr_。
#include <condition_variable>
#include <iostream>
#include <list>
#include <memory>
#include <mutex>
#include <vector>template <typename T>
class MsgPool {public:~MsgPool();template <typename... Args>static void Create(int max_alloc, Args &&... args) {std::lock_guard<std::mutex> lck(singleton_mutex_);pool_ptr_.reset(new MsgPool<T>);pool_ptr_->Init(max_alloc, std::forward<Args>(args)...);}static std::shared_ptr<T> GetSharedPtr(int timeout = -1) {MsgPool<T> *pool = GetInstance();if (!pool) {return nullptr;}std::unique_lock<std::mutex> lck(pool->pool_mutex_);if (pool->free_list_.empty()) {if (timeout > 0) {pool->item_available_cond_.wait_for(lck,std::chrono::milliseconds(timeout));if (pool->free_list_.empty()) {return nullptr;}} else {return nullptr;}}T *item = pool->free_list_.front();pool->free_list_.pop_front();pool->used_list_.push_back(item);if (!item) {return nullptr;}std::shared_ptr<T> sp_item(item, ItemDeleter);return sp_item;}int GetMaxAllocCnt() const { return max_alloc_; }size_t GetUsedCnt() const {std::lock_guard<std::mutex> lck(pool_mutex_);return used_list_.size();}size_t GetFreeCnt() const {std::lock_guard<std::mutex> lck(pool_mutex_);return free_list_.size();}private:MsgPool() = default;MsgPool(const MsgPool &) = delete;MsgPool &operator=(const MsgPool &) = delete;MsgPool(MsgPool &&) = delete;MsgPool &operator=(MsgPool &&) = delete;template <typename... Args>int Init(int max_alloc, Args &&... args);template <typename... Args>void AllocItem(Args &&... args);static MsgPool<T> *GetInstance() {std::lock_guard<std::mutex> lck(singleton_mutex_);if (!pool_ptr_) {std::cout << "please initialize MsgPool<" << typeid(T).name() << "> first"<< std::endl;return nullptr;}return pool_ptr_.get();}static void ItemDeleter(T *item) {if (!item) {return;}MsgPool<T> *pool = GetInstance();if (!pool) {return;}std::lock_guard<std::mutex> lck(pool->pool_mutex_);auto iter = find(pool->used_list_.begin(), pool->used_list_.end(), item);if (iter == pool->used_list_.end()) {return;}item->Reset();pool->free_list_.push_back(item);pool->used_list_.erase(iter);pool->item_available_cond_.notify_one();}private:std::vector<T *> items_;std::list<T *> free_list_;std::list<T *> used_list_;int max_alloc_{0};// ensure multi-threads safetymutable std::mutex pool_mutex_;// ensure multi-thread synchronizationstd::condition_variable item_available_cond_;// ensure singletonstatic std::mutex singleton_mutex_;static std::unique_ptr<MsgPool<T> > pool_ptr_;
};template <typename T>
std::mutex MsgPool<T>::singleton_mutex_;template <typename T>
std::unique_ptr<MsgPool<T> > MsgPool<T>::pool_ptr_;template <typename T>
MsgPool<T>::~MsgPool() {std::lock_guard<std::mutex> lck(pool_mutex_);for (size_t i = 0; i < items_.size(); i++) {delete items_[i];}items_.clear();free_list_.clear();used_list_.clear();
}template <typename T>
template <typename... Args>
int MsgPool<T>::Init(int max_alloc, Args &&... args) {std::lock_guard<std::mutex> lck(pool_mutex_);for (int i = 0; i < max_alloc; i++) {AllocItem(std::forward<Args>(args)...);}used_list_.clear();max_alloc_ = max_alloc;return 0;
}template <typename T>
template <typename... Args>
void MsgPool<T>::AllocItem(Args &&... args) {T *item = new T(std::forward<Args>(args)...);items_.push_back(item);free_list_.push_front(item);
}
使用该MsgPool:
#include <memory>
#include <thread>typedef MsgPool<Object> ObjectPool;void fun() {std::shared_ptr<Object> obj = nullptr;while (obj == nullptr) {obj = ObjectPool::GetSharedPtr();}obj->print();std::this_thread::sleep_for(std::chrono::milliseconds(2000));
}int main() {ObjectPool::Create(5);std::vector<std::thread> thread_vec;for (size_t i = 0; i < 8; ++i) {std::thread thr(fun);thread_vec.push_back(std::move(thr));}for (size_t i = 0; i < thread_vec.size(); ++i) {thread_vec[i].join();}return 0;
}
我们看到,对象池的大小为5,而使用该对象池的线程却有8个,那么必然会存在线程使用不到的情况。那么我们运行代码:
0xc99260 id_ : -1
0xc99220 id_ : -1
0xc99200 id_ : -1
0xc991c0 id_ : -1
0xc990d0 id_ : -1
object 0xc99260 Reset
0xc99260 id_ : -1
object 0xc99220 Reset
object 0xc991c0 Reset
0xc99220 id_ : -1
object 0xc99200 Reset
object 0xc990d0 Reset
0xc991c0 id_ : -1
object 0xc99220 Reset
object 0xc99260 Reset
object 0xc991c0 Reset
可以看到,0xc99260、0xc99220、0xc991c0这三个对象,在第一次使用完成后,调用Reset()进行重置,随后被剩下的3个线程申请到。这符合对象池的使用逻辑。
局部对象池
由于局部对象池中,没有对应全局对象池中的pool_ptr_,而在删除器ItemDeleter中,又必须对this指针进行调整。此时需要注意:在shared_ptr中,不能直接使用this指针,而是要使用shared_from_this(),并继承enable_shared_from_this类。
对该点不太熟悉的同学,可以参考博文:【C++】shared_ptr共享型智能指针详解。
#include <condition_variable>
#include <iostream>
#include <list>
#include <memory>
#include <mutex>
#include <vector>template <typename T>
class MsgPoolEx final : public std::enable_shared_from_this<MsgPoolEx<T>> {public:MsgPoolEx() = default;~MsgPoolEx();template <typename... Args>static std::shared_ptr<MsgPoolEx<T> > Create(int max_alloc,Args &&... args) {std::shared_ptr<MsgPoolEx<T> > sp_pool = std::make_shared<MsgPoolEx<T>>();sp_pool->Init(max_alloc, std::forward<Args>(args)...);return sp_pool;}std::shared_ptr<T> GetSharedPtr(int timeout = -1) {std::unique_lock<std::mutex> lck(pool_mutex_);if (free_list_.empty()) {if (timeout > 0) {item_available_cond_.wait_for(lck, std::chrono::milliseconds(timeout));if (free_list_.empty()) {return nullptr;}} else {return nullptr;}}T *item = free_list_.front();free_list_.pop_front();used_list_.push_back(item);if (!item) {return nullptr;}ItemDeleter deleter(this->shared_from_this());std::shared_ptr<T> sp_item(item, deleter);return sp_item;}int GetMaxAllocCnt() const { return max_alloc_; }size_t GetUsedCnt() const {std::lock_guard<std::mutex> lck(pool_mutex_);return used_list_.size();}size_t GetFreeCnt() const {std::lock_guard<std::mutex> lck(pool_mutex_);return free_list_.size();}private:MsgPoolEx(const MsgPoolEx &) = delete;MsgPoolEx &operator=(const MsgPoolEx &) = delete;MsgPoolEx(MsgPoolEx &&) = delete;MsgPoolEx &operator=(MsgPoolEx &&) = delete;template <typename... Args>int Init(int max_alloc, Args &&... args);template <typename... Args>int AllocItem(Args &&... args);struct ItemDeleter {explicit ItemDeleter(std::shared_ptr<MsgPoolEx<T> > pool_del): pool(std::move(pool_del)) {}std::shared_ptr<MsgPoolEx<T> > pool;void operator()(T *item) {if (!item) {return;}std::lock_guard<std::mutex> lck(pool->pool_mutex_);auto iter =std::find(pool->used_list_.begin(), pool->used_list_.end(), item);if (iter == pool->used_list_.end()) {return;}item->Reset();pool->free_list_.push_back(item);pool->used_list_.erase(iter);pool->item_available_cond_.notify_one();}};private:std::vector<T *> items_;std::list<T *> free_list_;std::list<T *> used_list_;int max_alloc_{0};// ensure multi-threads safetymutable std::mutex pool_mutex_;// ensure multi-thread synchronizationstd::condition_variable item_available_cond_;
};template <typename T>
MsgPoolEx<T>::~MsgPoolEx() {std::lock_guard<std::mutex> lck(pool_mutex_);for (auto *item : items_) {delete item;}items_.clear();free_list_.clear();used_list_.clear();
}template <typename T>
template <typename... Args>
int MsgPoolEx<T>::Init(int max_alloc, Args &&... args) {std::lock_guard<std::mutex> lck(pool_mutex_);for (int i = 0; i < max_alloc; i++) {AllocItem(std::forward<Args>(args)...);}used_list_.clear();max_alloc_ = max_alloc;return 0;
}template <typename T>
template <typename... Args>
int MsgPoolEx<T>::AllocItem(Args &&... args) {T *item = new T(std::forward<Args>(args)...);items_.push_back(item);free_list_.push_front(item);return 0;
}
使用该MsgPoolEx:
#include <memory>
#include <thread>typedef MsgPoolEx<Object> ObjectPoolEx;void fun() {std::shared_ptr<ObjectPoolEx> pool = ObjectPoolEx::Create(5);std::shared_ptr<Object> obj = nullptr;while (obj == nullptr) {obj = pool->GetSharedPtr();}obj->print();std::this_thread::sleep_for(std::chrono::milliseconds(2000));
}int main()
{std::vector<std::thread> thread_vec;for (size_t i = 0; i < 8; ++i) {std::thread thr(fun);thread_vec.push_back(std::move(thr));}for (size_t i = 0; i < thread_vec.size(); ++i) {thread_vec[i].join();}return 0;
}
我们看到,由于该对象池是局部对象池,尽管使用该对象池的线程有8个,但是可以每个线程都开辟一个专属于该线程的对象池。这样做,自然不会遇到线程不安全的问题。那么我们运行代码:
0x7f2b54000d60 id_ : -1
0x7f2b4c000d60 id_ : -1
0x7f2b44000d60 id_ : -1
0x7f2b48000d60 id_ : -1
0x7f2b34000d60 id_ : -1
0x7f2b50000d60 id_ : -1
0x7f2b3c000d60 id_ : -1
0x7f2b40000d60 id_ : -1
object 0x7f2b4c000d60 Reset
object 0x7f2b34000d60 Reset
object 0x7f2b48000d60 Reset
object 0x7f2b3c000d60 Reset
object 0x7f2b54000d60 Reset
object 0x7f2b40000d60 Reset
object 0x7f2b50000d60 Reset
object 0x7f2b44000d60 Reset
这符合对象池的使用逻辑。
相关阅读
- C++对象池的实现和原理
- Object Pool 对象池的C++11使用(转)
- 【C++】shared_ptr共享型智能指针详解