【C++】C++11多线程下可用的对象池

在我日常的项目中,对象池是使用场景非常广泛的一种手段。本文就对对象池的设计原理进行讲解,并对其底层进行抽象,提供一个可以快速、简单上手的对象池模板类。


对象池原理

对象池是一种空间换时间的技术,对象被预先创建并初始化后放入对象池中,对象提供者就能利用已有的对象来处理请求,并在不需要时归还给池子而非直接销毁。它减少对象频繁创建所占用的内存、空间和初始化时间。

描述一个对象池有两个很重要的参数,一个是这个对象池的类型,另一个是这个对象池可以获得对象的数量。

对象池的实现和内存池的实现原理很像:都是一开始申请大内存空间,然后把大内存分配成小内存空间,当需要使用的时候直接分配使用,不在向系统申请内存空间,也不直接释放内存空间。使用完之后都是放回池子里。

不同的地方在内存池有一个映射数组,在使用时负责快速定位合适的内存池(一个内存池可以有很多内存块大小不同的池子)。而每一个类型的对象只对应一个对象池,并自己管理自己的对象池。不同类型的对象池是相互独立的存在。

对象池的优点:

  1. 减少频繁创建和销毁对象带来的成本,实现对象的缓存和复用
  2. 提高了获取对象的响应速度,对实时性要求较高的程序有很大帮助;
  3. 一定程度上减少了垃圾回收机制(GC)的压力。

对象池的缺点:

  1. 很难设定对象池的大小,如果太小则不起作用,过大又会占用内存资源过高;
  2. 并发环境中,多个线程可能(同时)需要获取池中对象,进而需要在堆数据结构上进行同步或者因为锁竞争而产生阻塞,这种开销要比创建销毁对象的开销高数百倍;
  3. 由于池中对象的数量有限,势必成为一个可伸缩性瓶颈。

什么条件下,适合使用对象池?

  1. 资源受限的,不需要可伸缩性的环境(cpu\内存等物理资源有限):cpu性能不够强劲,内存比较紧张,垃圾收集,内存抖动会造成比较大的影响,需要提高内存管理效率,响应性比吞吐量更为重要。
  2. 数量受限的,比如数据库连接。
  3. 创建对象的成本比较大,并且创建比较频繁。比如线程的创建代价比较大,于是就有了常用的线程池。

对象池设计

本文将设计两种类型的对象池:全局对象池和局部对象池。所谓全局对象池,就是指利用单例的方式,确保某种类型的对象池,全局情况下共享一个;而局部对象池,则可以生成多个同种类型的对象池,然后可以在不同的局部进行使用

  • 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 safety
  mutable std::mutex pool_mutex_;
  // ensure multi-thread synchronization
  std::condition_variable item_available_cond_;

  // ensure singleton
  static 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 safety
  mutable std::mutex pool_mutex_;
  // ensure multi-thread synchronization
  std::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

这符合对象池的使用逻辑。


相关阅读

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值