源码地址
https://github.com/puzzzzzzle/algorithm_study/blob/master/algorithm_cpp/src/object_pool/ObjectPool.h
自动回收裸指针的对象池
原理
- c++ shared_ptr, 在析构时, 会调用deleter 策略, 一般用于 共享指针中存储数组
- 我们可以在这里做文章, 自定义deleter, 按照一定的策略, 选择释放内存还是回收内存
- 注意, 这种方式无法回收共享内存本身, 但是可以做到自动回收
- 适用于构造和析构消耗较大 的对象
获取对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ObjectT *AllocWithTrunk() { for (int i = 0; i < KEEP_SIZE / 10; ++i) { m_reusePool.Put(new ObjectT()); } return new ObjectT(); } ObjectPtrT Alloc() { ObjectT *rowPtr = m_reusePool.Take(); if (rowPtr == nullptr) { rowPtr = AllocWithTrunk(); } assert(rowPtr != nullptr); m_constructor(rowPtr); return std::shared_ptr<ObjectT>( rowPtr, [this](ObjectT *ptr) { ReleaseObject(ptr); }); }
|
从池中尝试获取一个, 没拿到的话就分配一批
- 目前的策略是分配最大池的大小的1/10, 可以调整
分配的时候, 拿到裸指针后, 构建共享指针, 同时指定自定义的deleter
自动回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| void ReleaseObject(ObjectT *ptr) { bool needReuse = true; if (m_isStopping) { needReuse = false; } if (m_reusePool.Size() > KEEP_SIZE) { needReuse = false; }
m_destructor(ptr); if (needReuse) {
m_reusePool.Put(ptr); } else { delete ptr; } }
|
- 当共享指针计数为0时, 会调用deleter, 回调到我们自定义的
ReleaseObject
中
- 首先调用已定义的析构策略
- 按照一定的策略决定要不要回收
- 不回收的直接delete
- 回收的 再次放回池子中
释放内存池
1 2 3 4 5 6 7 8 9
| ~ObjectPool() { m_isStopping = true; while (m_reusePool.Size() > 0) { auto *rowPtr = m_reusePool.Take(); delete rowPtr; } }
|
- 释放对象池时, 要销毁所有对象, 释放池子中的对象
- 这时候要停止回收
- 我们设置 m_isStopping 为true, 这时候后续的都不会被回收
自定义策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
template <typename Object, typename ReusePoolType = ThreadSafeQueuePoolType<Object *>, size_t KEEP_SIZE_NUM = 10000, typename Constructor = DefaultObjectConstructor<Object *>, typename Destructor = DefaultObjectDestructor<Object *>> class ObjectPool
|
- Object: 要存储的数据类型
- ReusePoolType: 用于存储池子中的对象, 默认是一个有锁的队列, 多线程安全性取决于它
- KEEP_SIZE_NUM : 决定池子保存的对象的最大大小和每次分配量
- Constructor : 从池子中获取一个对象前总会调用的策略
- Destructor : 一个对象共享指针计数为0 时总会调用, 无论是否放回池子
- 构造和析构策略满足下面的格式就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
template <typename Object> struct DefaultObjectConstructor { using ObjectT = Object; void operator()(ObjectT object) const {} };
template <typename Object> struct DefaultObjectDestructor { using ObjectT = Object; void operator()(ObjectT object) const {} };
|
多线程安全
这个对象池除了 对象存储外都是多线程安全的
- 因此, 只要 ReusePoolType 是多线程安全的, 那池子本身就是安全的
满足如下存储器策略即可
1.
1 2 3 4 5 6
| template <typename Object> struct PoolType { void Put(ObjectT ptr) ; ObjectT Take(); size_t Size(); };
|
默认提供一个基于锁的线程安全存储器, 无锁版本可以包装一个无锁队列就行
- 有需要的话可以使用 moodycamel 实现的无锁安全队列
- https://github.com/cameron314/concurrentqueue
1 2 3 4 5 6 7 8 9
|
template <typename Object> struct ThreadSafeQueuePoolType
|
手动回收共享指针的对象池
- 线程安全性取决于 PoolType 是否是线程安全的
- 连 shared_ptr 也一同回收,但是需要手动调用回收函数,如果回收时, 引用计数不为1, 就会放弃回收
- 回收后, 原来的指针会被置为null
- 如果一个对象没有被 ReleaseObject 那就不会调用 Destructor, 而是直接使用默认的析构函数
- 需要保证析构函数和 Destructor 都可以做到资源释放, 且二次释放没有问题
- 用于 对象量极大, 连shared_ptr的构造和析构都是瓶颈的地方
1 2 3 4 5 6 7
| template <typename Object, typename ReusePoolType = ThreadSafeQueuePoolType<std::shared_ptr<Object>>, size_t KEEP_SIZE_NUM = 10000, typename Constructor = DefaultObjectConstructor<Object *>, typename Destructor = DefaultObjectDestructor<Object *>> class ObjectManulPool
|
手动回收
- 使用完毕后需要手动调用
TryPushBack
1 2 3 4 5 6 7 8
| bool TryPushBack(ObjectPtrT &ptr) { if (ptr.use_count() == 1) { ReleaseObject(ptr); ptr = nullptr; return true; } return false; }
|
容错机制
- 会检查use_count, 只回收use_count() == 1的, 回收完原来的指针会被置为nullptr
- 保证还在使用的对象不会被回收
- 对于忘记回收的, 由 shared_ptr来保证进行收尾, 不会泄漏
- 即使吧不是由这个对象池构造的对象进行回收也没问题, 可以兼容