深圳幻海软件技术有限公司 欢迎您!

对象池的使用场景以及自动回收技术

2023-02-27

 对象池在编程中,我们经常会涉及到对象的操作,而经常的操作模式如下图所示:创建对象->使用对象->销毁对象。而这个对象有可能创建的时候会需要构建很多资源,消耗比较大,比如:在hiredis的SDK中每次都创建一个redisContext,如果需要查询,那就首先要进行网络连接。如

 对象池

在编程中,我们经常会涉及到对象的操作,而经常的操作模式如下图所示:创建对象->使用对象->销毁对象。

而这个对象有可能创建的时候会需要构建很多资源,消耗比较大, 比如:在hiredis的SDK中每次都创建一个redisContext,如果需要查询,那就首先要进行网络连接。如果一直都是上图的工作方式,那将会频繁的创建连接,查询完毕后再释放连接。重新建立连接,让网络的查询效率降低。

这个时候就可以构建一个对象池来重复利用这个对象,并且一般要做到线程安全:

  1. 从对象池中获取对象,如果没有对象,则创建一个,并返回
  2. 使用对象
  3. 使用完成对象后,将对象还回对象池

那么符合如下条件的,应该适合使用对象池技术:

  • 有一些对象虽然创建开销比较大,但是不一定能够重复使用。要使用对象池一定要确保对象能够重复使用。
  • 这个对象构建的时候,有一些耗时的资源可以重复利用。比如redisContext的网络连接。又或者如果对象的频繁申请释放会带来一些其他的资源使用问题,比如内存碎片。重复利用能够提升程序的效率。
  • 对象池的数量应该控制在能够接受的范围内,并不会无限膨胀。

对象池的实现

首先介绍一下程序的样例对象Object, 其就接受一个初始化参数strInit。

class Object 

public
  Object(std::string strInit) : m_strInit(strInit)  
  {  
    std::cout << "Object()" << std::endl;  
  } 
  virtual ~Object()  
  {  
    std::cout << "~Object()" << std::endl; 
  } 
private: 
  std::string m_strInit; 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

先来看看对象池的类图:

  • ObjectPool中采用std::list作为对象池的数据结构,存储的对象采用shared_ptr包裹。
  • GetObject获取一个对象,传入的参数为Object需要初始化的信息,如果池子里面没有,就创建一个返回,如果有就从池子中取出一个返回。
  • ReturnObject 当应用程序使用完毕后,调用这个方法还回对象到对象池

然后再来看看代码吧:

class ObjectPool 

public
  ObjectPool() { ; } 
  ~ObjectPool() { ; } 
  std::shared_ptr<Object> GetObject(std::string strInit) 
  { 
    std::shared_ptr<Object> pObject; 
    { 
      std::lock_guard<std::mutex> guard(m_mutex); 
      if (!m_lObjects.empty()) 
      { 
        pObject = m_lObjects.front(); 
        m_lObjects.pop_front(); 
      } 
    } 
 
    if (!pObject) 
    { 
      pObject = std::make_shared<Object>(strInit); 
    } 
    return pObject; 
  } 
 
  void ReturnObject(std::shared_ptr<Object> pObject) 

    if (!pObject) 
      return
 
    std::lock_guard<std::mutex> guard(m_mutex); 
    m_lObjects.push_front(pObject); 
  } 
 
private: 
  std::mutex m_mutex; 
  std::list<std::shared_ptr<Object>> m_lObjects; 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.

那么使用起来比较简单,如下所示。

ObjectPool objPool; 
  auto pObj1 = objPool.GetObject("abc"); 
  //操作对象完成任务 
  //...... 
  objPool.ReturnObject(pObj1); 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

但是要注意一点,有时候可能使用完了,却忘记调用ReturnObject了,这个时候是否想起了RAII技术《C++ RAII实现golang的defer》和《从lock_guard来说一说C++常用的RAII》。

那么问一问,可以实现一个自动回收的对象池吗?不需要调用者在对象使用完成后,手动将对象归还给对象池,并且你可能要问:

  1. 针对不同类型的Object,是不是可以用模板去实现更加通用的实现一个对象池
  2. 构造函数的参数列表,也可以是任意的形式

自动回收的对象池

要实现自动回收的对象池,首先要了解unique_ptr和shared_ptr都可以自定义删除器,也就是说,比如当从对象池获取到的对象是用智能指针包裹的,一般默认的删除器为delete,那我们可以自义定删除器为: 将这个对象重新放回到对象池. 代码如下:

template<typename T> 
class ObjectPool 

public
  ObjectPool() 
  { 
    m_fObjDeleter = [&](T* pObj) { 
      if (m_bDeconstruct) 
        delete pObj; 
      else 
      { 
        std::lock_guard<std::mutex> guard(m_mutex); 
        m_lObjects.push_front(std::shared_ptr<T>(pObj, m_fObjDeleter)); 
      } 
    }; 
  } 
 
  ~ObjectPool() 
  { 
    m_bDeconstruct = true
  } 
 
  template<typename... Args> 
  std::shared_ptr<T> GetObject(Args&&... args) 
  { 
    std::shared_ptr<T> pObject; 
    { 
      std::lock_guard<std::mutex> guard(m_mutex); 
      if (!m_lObjects.empty()) 
      { 
        pObject = m_lObjects.front(); 
        m_lObjects.pop_front(); 
      } 
    } 
 
    if (!pObject) 
    { 
      pObject.reset(new T(std::forward<Args>(args)...), m_fObjDeleter); 
    } 
    return pObject; 
  } 
 
  void ReturnObject(std::shared_ptr<T> pObject) 

    if (!pObject) 
      return
 
    std::lock_guard<std::mutex> guard(m_mutex); 
    m_lObjects.push_front(pObject); 
  } 
 
private: 
  std::function<void(T* pObj)> m_fObjDeleter; 
  std::mutex m_mutex; 
  std::list<std::shared_ptr<T>> m_lObjects; 
  volatile bool m_bDeconstruct = false
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.

自动回收

关于自动回收,这个涉及到一个问题,是用unique_ptr还是shared_ptr呢,在这篇大牛写的文章中进行了比较详细的阐述《thinking in object pool》(链接见参考部分), 说明了应该使用unique_ptr,也看到不少人在网上转发。主要如下阐述:

因为我们需要把智能指针的默认删除器改为自定义删除器,用shared_ptr会很不方便,因为你无法直接将shared_ptr的删除器修改为自定义删除器,虽然你可以通过重新创建一个新对象,把原对象拷贝过来的做法来实现,但是这样做效率比较低。而unique_ptr由于是独占语义,提供了一种简便的方法方法可以实现修改删除器,所以用unique_ptr是最适合的。

这种方式需要每次都创建一个新对象,并且拷贝原来的对象,是一种比较低效的做法。

但本人自己进行了思考,认为可以做到使用shared_ptr一样实现了高效的自动回收机制。首先定义了一个m_fObjDeleter自定义deleter, 不过这种做法可能比较难理解一些,就是定义的m_fObjDeleter函数内也会调用m_fObjDeleter。当shared_ptr引用计数为0的时候,会做如下事情:

  • 如果发现是OjbectPool调用了析构函数,则直接释放对象
  • 如果发现OjbectPool并没有调用析构函数,则将对象放入对象池中
m_fObjDeleter = [&](T* pObj) { 
  if (m_bDeconstruct) 
    delete pObj; 
  else 
  { 
    std::lock_guard<std::mutex> guard(m_mutex); 
    m_lObjects.push_front(std::shared_ptr<T>(pObj, m_fObjDeleter)); 
  } 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

当创建对象的时候指定自定义的deleter:

pObject.reset(new T(std::forward<Args>(args)...), m_fObjDeleter); 
  • 1.

模板支持

使用了模板可以支持通用的对象:

template<typename T> 
class ObjectPool 

public
    //...... 
    template<typename... Args> 
    std::shared_ptr<T> GetObject(Args&&... args) 
    { 
        //...... 
    } 
 
    void ReturnObject(std::shared_ptr<T> pObject) 
    { 
        //...... 
    } 
 
private: 
    std::function<void(T* pObj)> m_fObjDeleter; 
    //..... 
    std::list<std::shared_ptr<T>> m_lObjects; 
    //....... 
}; 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.

可变函数参数完美转发

不同的对象,可能使用的构造函数参数也不同,那么当调用GetObject的时候的参数要设置为可变参数,其实现如下:

template<typename... Args> 
std::shared_ptr<T> GetObject(Args&&... args) 

  std::shared_ptr<T> pObject; 
  { 
    std::lock_guard<std::mutex> guard(m_mutex); 
    if (!m_lObjects.empty()) 
    { 
      pObject = m_lObjects.front(); 
      m_lObjects.pop_front(); 
    } 
  } 
 
  if (!pObject) 
  { 
    pObject.reset(new T(std::forward<Args>(args)...), m_fObjDeleter); 
  } 
  return pObject; 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

其他

以上对对象池的基本内容进行了阐述,那么对于对象池的实现要根据场景还有若干的细节,有些还比较重要:

  • 是否要在启动的时候初始化指定数量的对象?
  • 对象池的数量是否要设置一个上限或者下线
  • 对象池重复利用,当取出来后要注意,是不是要对对象做一次reset之类的操作,防止对象上一次的调用残留数据对本地调用构成影响,这个要根据自己对象的特点去进行相应的reset操作
  • 有时候当这个对象可能出现了特别的情况需要销毁,是否也需要考虑到?
  • 等等

参考

  • <<C++ Primer>>模板部分
  • << thinking in object pool >>: https://www.cnblogs.com/qicosmos/p/4995248.html