专栏首页一个程序员的修炼之路对象池的使用场景以及自动回收技术

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

对象池

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

而这个对象有可能创建的时候会需要构建很多资源,消耗比较大, 比如:在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;
};

先来看看对象池的类图

  • 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;
};

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

  ObjectPool objPool;
  auto pObj1 = objPool.GetObject("abc");
  //操作对象完成任务
  //......
  objPool.ReturnObject(pObj1);

但是要注意一点,有时候可能使用完了,却忘记调用ReturnObject了,这个时候是否想起了RAII技术《C++ RAII实现golang的defer》《从lock_guard来说一说C++常用的RAII》。 那么问一问,可以实现一个自动回收的对象池吗?不需要调用者在对象使用完成后,手动将对象归还给对象池,并且你可能要问:

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

自动回收的对象池

要实现自动回收的对象池,首先要了解unique_ptrshared_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;
};

自动回收

关于自动回收,这个涉及到一个问题,是用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));
  }
};

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

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

模板支持

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

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;
	//.......
};

可变函数参数完美转发

不同的对象,可能使用的构造函数参数也不同,那么当调用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;
}

其他

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

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

参考

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

本文分享自微信公众号 - 一个程序员的修炼之路(CoderStudyShare),作者:河边一枝柳

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 技术分享 | kafka的使用场景以及生态系统

    kafka的使用场景 今天介绍一些关于Apache kafka 流行的使用场景。这些领域的概述 消息 kafka更好的替换传统的消息系统,消息系统被用于各种场景...

    加米谷大数据
  • 双面银隆

    镁客网
  • Java面试 32个核心必考点完全解析

    1~3年内从工程师到高级工程师发展,夯实基础,重点提高工作基础能力,培养技术的深度和广度,对不同方向的新技术保持强烈的好奇心和学习心

    本人秃顶程序员
  • Unity基础教程系列(三)——复用对象(Object Pools)

    如果我们只能创造形状,那么它们的数量只会增加,直到我们开始一个新的游戏为止。但大部分的时候,当一些物体在游戏中被创建时,它也应该可以被销毁。现在让我们让销毁形状...

    放牛的星星
  • 一位资深Java架构师的晋级心得

    Java 架构师是什么?是一个既需要掌控整体又需要洞悉局部瓶颈并依据具体的业务场景给出解决方案的团队领导型人物。一个架构师得需要足够的想像力,能把各种目标需求进...

    Debian中国
  • JAVA程序员备战跳槽季,准备面试必备的技术大纲,请查收

    怎么来体现你的技术实力?我总的分为:技术深度和技术广度这两方面。技术广度通俗的讲,就是你熟悉该技术点的使用以及基本原理。一般面试官在面试首轮会问很多技术点,来考...

    美的让人心动
  • 面试BAT时,他们问了我这些!

    能进入BAT 等一线大厂工作,是很多开发者们的目标与梦想,为帮助开发者们提升面试技能、高效通关一线互联网公司的面试,提炼总结了这份面试真题,一次整体放出送给大家...

    JavaEdge
  • RPC 实战总结与进阶延伸

    ByteBuf 是必须要掌握的核心工具类,并且能够理解 ByteBuf 的内部构造。ByteBuf 包含三个指针:读指针 readerIndex、写指针 wri...

    MickyInvQ
  • 对象池模式&解释器模式

    设计模式系列文章之前已经跟大家聊了一大半了,但是都是聊一些比较常见的设计模式,接下来我们主要是聊一些不常见的设计模式。

    敖丙
  • 干货|18张图揭秘高性能Linux服务器内存池技术是如何实现的

    大家生活中肯定都有这样的经验,那就是大众化的产品都比较便宜,但便宜的大众产品就是一个词,普通;而可以定制的产品一般都价位不凡,这种定制的产品注定不会在大众中普及...

    CloudBest
  • Elasticsearch 最佳实践系列之分片恢复并发故障

    提示:公众号展示代码会自动折行,建议横屏阅读 ---- 大家好,今天为大家分享一次 ES 的填坑经验。主要是关于集群恢复过程中,分片恢复并发数调整过大导致集...

    腾讯数据库技术
  • 金九银十Offer收割机:Android 面试核心知识点精讲,不打没准备的仗!

    无他,就是靠自己的毅力以及决心。一天不行,一个月;一个月不行,一年;有决心的人,啥学历、或者资历,那些都是借口。

    Android技术干货分享
  • 字节跳动面试题

    一个会写诗的程序员
  • 再刷一波起来!Java后端开发面经大集锦2.0,刷完顺利拿下Offer!

    昨天场主献上Java后端开发面经大集锦1.0,反响特别好!还有程序员“指控”场主:为啥不早点推送??并送上了一个意味深长的微笑

    养码场
  • Unity 实战项目 ☀️| Unity中的对象池技术 ObjectPool 定义 + 实例演示

    对象池是一种Unity经常用到的内存管理服务,针对需要经常生成消失的对象,作用在于可以减少创建每个对象的系统开销。我们在对象需要消失的时候不Destroy而是S...

    呆呆敲代码的小Y
  • org.apache.commons.pool 对象池

    创建新的对象并初始化的操作,可能会消耗很多的时间。在需要频繁创建并使用这些对象的场景中,为了提供系统性能,通常的做法是,创建一个对象池,将一定数量的对象缓存到这...

    IT云清
  • 数字化营销时代:企业如何从“推时代”进阶“拉时代”

    随着互联网经济形态由消费到产业的进阶迭代,业务场景及商业逻辑从“推营销”时代向“拉营销”时代转变,推时代即平台利用信息推送的方式来获取和维系客户,拉时代则是平台...

    盈鱼MA
  • 十一月面试总结

    11月月初,从工作一年的公司离职了。离职后,休息了三天开始投简历、找工作,第一天面了花儿绽放,挂在了技术面,第二天面了金蝶,拿到了offer(顺便说一下,大公司...

    小诸葛
  • Java线程池详解

    构造一个线程池为什么需要几个参数?如果避免线程池出现OOM?Runnable和Callable的区别是什么?本文将对这些问题一一解答,同时还将给出使用线程池的常...

    java架构师

扫码关注云+社区

领取腾讯云代金券