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