暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

一个程序员的修炼之路 2021-08-29
932

对象池

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

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


          自动回收

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

                文章转载自一个程序员的修炼之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                评论