0%

c++自动回收对象池

源码地址

https://github.com/puzzzzzzle/algorithm_study/blob/master/algorithm_cpp/src/object_pool/ObjectPool.h

自动回收裸指针的对象池

原理

  1. c++ shared_ptr, 在析构时, 会调用deleter 策略, 一般用于 共享指针中存储数组
  2. 我们可以在这里做文章, 自定义deleter, 按照一定的策略, 选择释放内存还是回收内存
  3. 注意, 这种方式无法回收共享内存本身, 但是可以做到自动回收
  4. 适用于构造和析构消耗较大 的对象

获取对象

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. 从池中尝试获取一个, 没拿到的话就分配一批

    1. 目前的策略是分配最大池的大小的1/10, 可以调整
  2. 分配的时候, 拿到裸指针后, 构建共享指针, 同时指定自定义的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) {
// 回收一个对象

// 这里不会有问题, 前一个shared_ptr 调用了 deleter 后, 就不再管这个指针了,
// 只要我们没有真的delete, 相当与这个指针泄漏了
// 但是这里把这个指针保存起来了, 下次再用, 就没有泄漏
m_reusePool.Put(ptr);
} else {
// 直接释放, 不再回收
delete ptr;
}
}
  1. 当共享指针计数为0时, 会调用deleter, 回调到我们自定义的ReleaseObject
  2. 首先调用已定义的析构策略
  3. 按照一定的策略决定要不要回收
    1. 不回收的直接delete
    2. 回收的 再次放回池子中

释放内存池

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;
}
}
  1. 释放对象池时, 要销毁所有对象, 释放池子中的对象
  2. 这时候要停止回收
    1. 我们设置 m_isStopping 为true, 这时候后续的都不会被回收

自定义策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 自动回收的对象池实现
* 线程安全性取决于 PoolType 是否是线程安全的
* 只回收裸指针, shared_ptr不会回收
* @tparam Object 要存储的数据类型
* @tparam PoolType 用于存储池子中的对象, 默认是一个有锁的队列, 多线程安全性取决于它
* @tparam KEEP_SIZE_NUM 决定池子保存的对象的最大大小和每次分配量
* @tparam Constructor 从池子中获取一个对象前总会调用的策略
* @tparam Destructor 一个对象共享指针计数为0 时总会调用, 无论是否放回池子
*/
template <typename Object,
typename ReusePoolType = ThreadSafeQueuePoolType<Object *>,
size_t KEEP_SIZE_NUM = 10000,
typename Constructor = DefaultObjectConstructor<Object *>,
typename Destructor = DefaultObjectDestructor<Object *>>
class ObjectPool
  1. Object: 要存储的数据类型
  2. ReusePoolType: 用于存储池子中的对象, 默认是一个有锁的队列, 多线程安全性取决于它
  3. KEEP_SIZE_NUM : 决定池子保存的对象的最大大小和每次分配量
  4. Constructor : 从池子中获取一个对象前总会调用的策略
  5. Destructor : 一个对象共享指针计数为0 时总会调用, 无论是否放回池子
  6. 构造和析构策略满足下面的格式就行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 每个对象被get 时 调用的构造方法
* @tparam Object
*/
template <typename Object>
struct DefaultObjectConstructor {
using ObjectT = Object;
void operator()(ObjectT object) const {}
};
/**
* 每个对象被 release, 重新放回 pool 时 调用的析构方法
* @tparam Object
*/
template <typename Object>
struct DefaultObjectDestructor {
using ObjectT = Object;
void operator()(ObjectT object) const {}
};

多线程安全

  1. 这个对象池除了 对象存储外都是多线程安全的

    1. 因此, 只要 ReusePoolType 是多线程安全的, 那池子本身就是安全的
  2. 满足如下存储器策略即可
    1.

    1
    2
    3
    4
    5
    6
    template <typename Object>
    struct PoolType {
    void Put(ObjectT ptr) ;
    ObjectT Take();
    size_t Size();
    };
  3. 默认提供一个基于锁的线程安全存储器, 无锁版本可以包装一个无锁队列就行

    1. 有需要的话可以使用 moodycamel 实现的无锁安全队列
    2. https://github.com/cameron314/concurrentqueue
1
2
3
4
5
6
7
8
9
/**
* 线程安全的队列池
* 锁实现的性能较差
* 有需要的话可以使用 moodycamel 实现的无锁安全队列
* https://github.com/cameron314/concurrentqueue
* @tparam Object
*/
template <typename Object>
struct ThreadSafeQueuePoolType

手动回收共享指针的对象池

  1. 线程安全性取决于 PoolType 是否是线程安全的
  2. 连 shared_ptr 也一同回收,但是需要手动调用回收函数,如果回收时, 引用计数不为1, 就会放弃回收
  3. 回收后, 原来的指针会被置为null
  4. 如果一个对象没有被 ReleaseObject 那就不会调用 Destructor, 而是直接使用默认的析构函数
  5. 需要保证析构函数和 Destructor 都可以做到资源释放, 且二次释放没有问题
  6. 用于 对象量极大, 连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

手动回收

  1. 使用完毕后需要手动调用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;
}

容错机制

  1. 会检查use_count, 只回收use_count() == 1的, 回收完原来的指针会被置为nullptr
    1. 保证还在使用的对象不会被回收
  2. 对于忘记回收的, 由 shared_ptr来保证进行收尾, 不会泄漏
  3. 即使吧不是由这个对象池构造的对象进行回收也没问题, 可以兼容