對象池的使用場景以及自動回收技術
對象池
在編程中,我們經(jīng)常會涉及到對象的操作,而經(jīng)常的操作模式如下圖所示:創(chuàng)建對象->使用對象->銷毀對象。
而這個對象有可能創(chuàng)建的時候會需要構建很多資源,消耗比較大, 比如:在hiredis的SDK中每次都創(chuàng)建一個redisContext,如果需要查詢,那就首先要進行網(wǎng)絡連接。如果一直都是上圖的工作方式,那將會頻繁的創(chuàng)建連接,查詢完畢后再釋放連接。重新建立連接,讓網(wǎng)絡的查詢效率降低。
這個時候就可以構建一個對象池來重復利用這個對象,并且一般要做到線程安全:
- 從對象池中獲取對象,如果沒有對象,則創(chuàng)建一個,并返回
- 使用對象
- 使用完成對象后,將對象還回對象池
那么符合如下條件的,應該適合使用對象池技術:
- 有一些對象雖然創(chuàng)建開銷比較大,但是不一定能夠重復使用。要使用對象池一定要確保對象能夠重復使用。
- 這個對象構建的時候,有一些耗時的資源可以重復利用。比如redisContext的網(wǎng)絡連接。又或者如果對象的頻繁申請釋放會帶來一些其他的資源使用問題,比如內(nèi)存碎片。重復利用能夠提升程序的效率。
- 對象池的數(shù)量應該控制在能夠接受的范圍內(nèi),并不會無限膨脹。
對象池的實現(xiàn)
首先介紹一下程序的樣例對象Object, 其就接受一個初始化參數(shù)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作為對象池的數(shù)據(jù)結構,存儲的對象采用shared_ptr包裹。
- GetObject獲取一個對象,傳入的參數(shù)為Object需要初始化的信息,如果池子里面沒有,就創(chuàng)建一個返回,如果有就從池子中取出一個返回。
- ReturnObject 當應用程序使用完畢后,調(diào)用這個方法還回對象到對象池
然后再來看看代碼吧:
- 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);
但是要注意一點,有時候可能使用完了,卻忘記調(diào)用ReturnObject了,這個時候是否想起了RAII技術《C++ RAII實現(xiàn)golang的defer》和《從lock_guard來說一說C++常用的RAII》。
那么問一問,可以實現(xiàn)一個自動回收的對象池嗎?不需要調(diào)用者在對象使用完成后,手動將對象歸還給對象池,并且你可能要問:
- 針對不同類型的Object,是不是可以用模板去實現(xiàn)更加通用的實現(xiàn)一個對象池
- 構造函數(shù)的參數(shù)列表,也可以是任意的形式
自動回收的對象池
要實現(xiàn)自動回收的對象池,首先要了解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,也看到不少人在網(wǎng)上轉(zhuǎn)發(fā)。主要如下闡述:
因為我們需要把智能指針的默認刪除器改為自定義刪除器,用shared_ptr會很不方便,因為你無法直接將shared_ptr的刪除器修改為自定義刪除器,雖然你可以通過重新創(chuàng)建一個新對象,把原對象拷貝過來的做法來實現(xiàn),但是這樣做效率比較低。而unique_ptr由于是獨占語義,提供了一種簡便的方法方法可以實現(xiàn)修改刪除器,所以用unique_ptr是最適合的。
…
這種方式需要每次都創(chuàng)建一個新對象,并且拷貝原來的對象,是一種比較低效的做法。
但本人自己進行了思考,認為可以做到使用shared_ptr一樣實現(xiàn)了高效的自動回收機制。首先定義了一個m_fObjDeleter自定義deleter, 不過這種做法可能比較難理解一些,就是定義的m_fObjDeleter函數(shù)內(nèi)也會調(diào)用m_fObjDeleter。當shared_ptr引用計數(shù)為0的時候,會做如下事情:
- 如果發(fā)現(xiàn)是OjbectPool調(diào)用了析構函數(shù),則直接釋放對象
- 如果發(fā)現(xiàn)OjbectPool并沒有調(diào)用析構函數(shù),則將對象放入對象池中
- 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));
- }
- };
當創(chuàng)建對象的時候指定自定義的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;
- //.......
- };
可變函數(shù)參數(shù)完美轉(zhuǎn)發(fā)
不同的對象,可能使用的構造函數(shù)參數(shù)也不同,那么當調(diào)用GetObject的時候的參數(shù)要設置為可變參數(shù),其實現(xiàn)如下:
- 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;
- }
其他
以上對對象池的基本內(nèi)容進行了闡述,那么對于對象池的實現(xiàn)要根據(jù)場景還有若干的細節(jié),有些還比較重要:
- 是否要在啟動的時候初始化指定數(shù)量的對象?
- 對象池的數(shù)量是否要設置一個上限或者下線
- 對象池重復利用,當取出來后要注意,是不是要對對象做一次reset之類的操作,防止對象上一次的調(diào)用殘留數(shù)據(jù)對本地調(diào)用構成影響,這個要根據(jù)自己對象的特點去進行相應的reset操作
- 有時候當這個對象可能出現(xiàn)了特別的情況需要銷毀,是否也需要考慮到?
- 等等
參考
- <<C++ Primer>>模板部分
- << thinking in object pool >>: https://www.cnblogs.com/qicosmos/p/4995248.html
































