<acronym id="s8ci2"><small id="s8ci2"></small></acronym>
<rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
<acronym id="s8ci2"></acronym>
<acronym id="s8ci2"><center id="s8ci2"></center></acronym>
0
  • 聊天消息
  • 系統消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術視頻
  • 寫文章/發帖/加入社區
會員中心
創作中心

完善資料讓更多小伙伴認識你,還能領取20積分哦,立即完善>

3天內不再提示

基于shared_ptr的C++非侵入式引用計數解決方案的缺陷

Linux愛好者 ? 來源:Linux愛好者 ? 作者:Linux愛好者 ? 2022-06-13 14:24 ? 次閱讀

問題描述

在基于C++的大型系統的設計實現中,由于缺乏語言級別的GC支持,資源生存周期往往是一個棘手的問題。系統地解決這個問題的方法無非兩種:

  • 使用GC庫
  • 使用引用計數

嚴格地說,引用計數其實也是一種最樸素的GC。相對于現代的GC技術,引用計數的實現簡單,但相應地,它也存在著循環引用和線程同步開銷等問題。

關于這二者孰優孰劣,已經有過很多討論,在此就不攪這股混水了。我一直也沒有使用過C++的GC庫,在實際項目中總是采用引用計數的方案。而作為Boost的擁躉,首選的自然是shared_ptr。一直以來我也對shared_ptr百般推崇,然而最近的一些項目開發經驗卻讓我在shared_ptr上栽了坑,對C++引用計數也有了一些新的的認識,遂記錄在此。

本文主要針對基于boost::shared_ptr的C++引用計數實現方案進行一些討論。C++引用計數方案往往伴隨著用于自動管理引用計數的智能指針。按是否要求資源對象自己維護引用計數,C++引用計數方案可以分為兩類:

  • 侵入式:侵入式的引用計數管理要求資源對象本身維護引用計數,同時提供增減引用計數的管理接口。通常侵入式方案會提供配套的侵入式引用計數智能指針。該智能指針通過調用資源對象的引用計數管理接口來自動增減引用計數。COM對象與CComPtr便是侵入式引用計數的一個典型實例。
  • 非侵入式:非侵入式的引用計數管理對資源對象本身沒有任何要求,而是完全借助非侵入式引用計數智能指針在資源對象外部維護獨立的引用計數。shared_ptr便是基于這個思路。

第一宗罪

初看起來,非侵入式方案由于對資源對象的實現沒有任何要求,相較于侵入式方案更具吸引力。然而事實卻并非如此。下面就來分析一下基于shared_ptr的非侵入式引用計數。在使用shared_ptr的引用計數解決方案中,引用計數完全由shared_ptr控制,資源對象對與自己對應的引用計數一無所知。

而引用計數與資源對象的生存期息息相關,這就意味著資源對象喪失了對生存期的控制權,將自己的生殺大權拱手讓給了shared_ptr。這種情況下,資源對象就不得不依靠至少一個shared_ptr實例來保障自己的生存。換言之,資源對象一旦“沾染”了shared_ptr,就一輩子都無法擺脫!考察以下的簡單用例:

用例一:

Resource*p=newCResource;
{
shared_ptrq(p);
}
p->Use()//CRASH

單純為了解決上述的崩潰,可以自定義一個什么也不做的deleter:

structnoop_deleter{
voidoperator()(void*){
//NO-OP
}
};

然后將上述用例的第三行改為:

shared_ptrq(p,noop_deleter());

但是這樣一來,shared_ptr就喪失了借助RAII自動釋放資源的能力,違背了我們利用智能指針自動管理資源生存期的初衷(話說回來,這倒并不是說noop_deleter這種手法毫無用處,Boost.Asio中就巧妙地利用shared_ptr、weak_ptr和noop_deleter來實現異步I/O事件的取消)。

從這個簡單的用例可以看出,shared_ptr就像是毒品一樣,一旦沾染就難以戒除。更甚者,染毒者連換用其他“毒品”的權力都沒有:shared_ptr的引用計數管理接口是私有的,無法從shared_ptr之外操控,也就無法從shared_ptr遷移到其他類型的引用計數智能指針。

不僅如此,資源對象沾染上shared_ptr之后,就只能使用最初的那個shared_ptr實例的拷貝來維系自己的生存期??疾煲韵掠美河美?/p>

{
shared_ptrp1(newCResource);
shared_ptrp2(p1);//OK
CResource*p3=p1.get();
shared_ptrp4(p3);//ERROR
//CRASH
}

該用例的執行過程如下:

  1. p1在構造的同時為資源對象創建了一份外部引用計數,并將之置為1
  2. p2拷貝自p1,與p1共享同一個引用計數,將之增加為2
  3. p4并非p1的拷貝,因此在構造的同時又為資源對象創建了另外一個外部引用計數,并將之置為1
  4. 在作用域結束時,p4析構,由其維護的額外的引用計數降為0,導致資源對象被析構
  5. 然后p2析構,對應的引用計數降為1
  6. 接著p1析構,對應的引用計數也歸零,于是p1在臨死之前再次釋放資源對象 最后,由于資源對象被二次釋放,程序崩潰

至此,我們已經認識到了shared_ptr的第一宗罪——傳播毒品

  • 毒性一:一旦開始對資源對象使用shared_ptr,就必須一直使用
  • 毒性二:無法換用其他類型的引用計數之智能指針來管理資源對象生存期
  • 毒性三:必須使用最初的shared_ptr實例拷貝來維系資源對象生存期

第二宗罪

乘勝追擊,再揭露一下shared_ptr的第二宗罪——散布病毒。有點聳人聽聞了?其實道理很簡單:由于使用了shared_ptr的資源對象必須仰仗shared_ptr的存在才能維系生存期,這就意味著使用資源的客戶對象也必須使用shared_ptr來持有資源對象的引用——于是shared_ptr的勢力范圍成功地從資源對象本身擴散到了資源使用者,侵入了資源客戶對象的實現。

同時,資源的使用者往往是通過某種形式的資源分配器來獲取資源。自然地,為了向客戶轉交資源對象的所有權,資源分配器也不得不在接口中傳遞shared_ptr,于是shared_ptr也會侵入資源分配器的接口。

有一種情況可以暫時擺脫shared_ptr,例如:

shared_ptrAllocateResource(){
shared_ptrpResource(newCResource);
InitResource(pResource.get());
returnpResource;
}
voidInitResource(IResource*r){
//Doresourceinitialization...
}

以上用例中,在InitResource的執行期間,由于AllocateResource的堆棧仍然存在,pResource不會析構,因此可以放心的在InitResource的參數中使用裸指針傳遞資源對象。這種基于調用棧的引用計數優化,也是一種常用的手段。但在InitResource返回后,資源對象終究還是會落入shared_ptr的魔掌。

由此可以看出,shared_ptr打著“非侵入式”的幌子,雖然沒有侵入資源對象的實現,卻侵入了資源分配接口以及資源客戶對象的實現。而沾染上shared_ptr就擺脫不掉,如此傳播下去,簡直就是侵入了除資源對象實現以外的其他各個地方!這不是病毒是什么?

然而,基于shared_ptr的引用計數解決方案真的不會侵入資源對象的實現嗎?

第三宗罪

在一些用例中,資源對象的成員方法(不包括構造函數)需要獲取指向對象自身,即包含了this指針的shared_ptr。Boost.Asio的chat示例便展示了這樣一個用例:chat_session對象會在其成員函數中發起異步I/O操作,并在異步I/O操作回調中保存一個指向自己的shared_ptr以保證回調執行時自身的生存期尚未結束。

這種手法在Boost.Asio中非常常見,在不考慮shared_ptr帶來的麻煩時,這實際上也是一種相當優雅的異步流程資源生存期處理方法。但現在讓我們把注意力集中在shared_ptr上。

通常,使用shared_ptr的資源對象必須動態分配,最常見的就是直接從堆上new出一個實例并交付給一個shared_ptr,或者也可以從某個資源池中分配再借助自定義的deleter在引用計數歸零時將資源放回池中。無論是那種用法,該資源對象的實例在創建出來后,都總是立即交付給一個shared_ptr(記為p)。

有鑒于之前提到的毒性三,如果資源對象的成員方法需要獲取一個指向自己的shared_ptr,那么這個shared_ptr也必須是p的一個拷貝——或者更本質的說,必須與p共享同一個外部引用計數。然而對于資源對象而言,p維護的引用計數是外部的陌生事物,資源對象如何得到這個引用計數并由此構造出一個合法的shared_ptr呢?這是一個比較tricky的過程。為了解決這個問題,Boost提供了一個類模板enable_shared_from_this:

所有需要在成員方法中獲取指向this的shared_ptr的類型,都必須以CRTP手法繼承自enable_shared_from_this。即:

classCResource:
publicboost::enable_shared_from_this
{
//...
};

接著,資源對象的成員方法就可以使用enable_shared_from_this::shared_from_this()方法來獲取所需的指向對象自身的shared_ptr了。問題似乎解決了。但是,等等!這樣的繼承體系不就對資源對象的實現有要求了嗎?換言之,這不正是對資源對象實現的赤裸裸的侵入嗎?這正是shared_ptr的第三宗罪——欺世盜名。

第四宗罪

最后一宗罪,是鋪張浪費。對了,說的就是性能。

基于引用計數的資源生存期管理,打一出生起就被扣著線程同步開銷大的帽子。早期的Boost版本中,shared_ptr是借助Boost.Thread的mutex對象來保護引用計數。在后期的版本中采用了lock-free的原子整數操作一定程度上降低了線程同步開銷。然而即使是lock-free,本質上也仍然是串行化訪問,線程同步的開銷多少都會存在。

也許有人會說這點開銷與引用計數帶來的便利相比算不得什么。然而在我們項目的異步服務器框架的壓力測試中,大量引用計數的增減操作,一舉吃掉了5%的CPU。換言之,1/20的計算能力被浪費在了與業務邏輯完全無關的引用計數的維護上!而且,由于是異步流程的特殊性,也無法應用上面提及的基于調用棧的引用計數優化。

那么針對這個問題就真的沒有辦法了嗎?其實仔細檢視一下整個異步流程,有些資源雖然會先后被不同的對象所引用,但在其整個生存周期內,每一時刻都只有一個對象持有該資源的引用。用于數據收發的緩沖區對象就是一個典型。它們總是被從某個源頭產生,然后便一直從一處被傳遞到另一處,最終在某個時刻被回收。

對于這樣的對象,實際上沒有必要針對流程中的每一次所有權轉移都進行引用計數操作,只要簡單地在分配時將引用計數置1,在需要釋放時再將引用計數歸零便可以了。

對于侵入式引用計數方案,由于資源對象自身持有引用計數并提供了引用計數的操作接口,可以很容易地實現這樣的優化。但shared_ptr則不然。shared_ptr把引用計數牢牢地攥在手中,不讓外界碰觸;外界只有通過shared_ptr的構造函數、析夠函數以及reset()方法才能夠間接地對引用計數進行操作。

而由于shared_ptr的毒品特性,資源對象無法脫離shared_ptr而存在,因此在轉移資源對象的所有權時,也必須通過拷貝shared_ptr的方式進行。一次拷貝就對應一對引用計數的原子增減操作。

對于上述的可優化資源對象,如果在一個流程中被傳遞3次,除去分配和釋放時的2次,還會導致6次無謂的原子整數操作。整整浪費了300%!

事實證明,在將基于shared_ptr的非侵入式引用計數方案更改為侵入式引用計數方案并施行上述優化后,我們的異步服務器框架的性能有了明顯的提升。

結語

最后總結一下shared_ptr的四宗罪:

  • 傳播毒品一旦對資源對象染上了shared_ptr,在其生存期內便無法擺脫。
  • 散布病毒在應用了shared_ptr的資源對象的所有權變換的整個過程中的所有接口都會受到shared_ptr的污染。
  • 欺世盜名在enable_shared_from_this用例下,基于shared_ptr的解決方案并非是非侵入式的。
  • 鋪張浪費由于shared_ptr隱藏了引用計數的操作接口,只能通過拷貝shared_ptr的方式間接操縱引用計數,使得用戶難以規避不必要的引用計數操作,造成無謂的性能損失。

探明這四宗罪算是最近一段時間的項目設計開發過程的一大收獲。寫這篇文章的目的不是為了將shared_ptr一棒子打死,只是為了總結基于shared_ptr的C++非侵入式引用計數解決方案的缺陷,也讓自己不再盲目迷信shared_ptr。

原文標題:共享指針四宗罪

文章出處:【微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。

審核編輯:湯梓紅
聲明:本文內容及配圖由入駐作者撰寫或者入駐合作網站授權轉載。文章觀點僅代表作者本人,不代表電子發燒友網立場。文章及其配圖僅供工程師學習之用,如有內容侵權或者其他違規問題,請聯系本站處理。 舉報投訴
  • 指針
    +關注

    關注

    1

    文章

    474

    瀏覽量

    70386
  • C++
    C++
    +關注

    關注

    21

    文章

    2068

    瀏覽量

    72972
  • 計數
    +關注

    關注

    1

    文章

    56

    瀏覽量

    20002

原文標題:共享指針四宗罪

文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏

    評論

    相關推薦

    記一次詭異的內存泄漏

    最近在補一些基礎知識,恰好涉及到了智能指針std::weak_ptr在解決std::shared_ptr時候循環引用的問題
    的頭像 發表于 02-19 13:44 ?350次閱讀
    記一次詭異的內存泄漏

    藍天工控接觸IC卡收費系統解決方案

    藍天工控接觸IC卡收費系統解決方案http://cnbpq.com 一、接觸IC卡收費系統簡介   隨著信息技術的不斷發展,社會經濟
    發表于 07-28 13:49

    C++標準庫第二版》總結濃縮精華

    。(2)scoped_array:與scoped_ptr的唯一不同是動態數組,單個對象。除非對性能有非??量桃?,或編譯器不支持標準庫,否則不建議使用。(3)shared_ptr:與unique_
    發表于 10-23 20:31

    《Boost程序完全開發指南》重點總結分享

    )scoped_array:與scoped_ptr的唯一不同是動態數組,單個對象。除非對性能有非??量桃?,或編譯器不支持標準庫,否則不建議使用。(3)shared_ptr:與unique_
    發表于 10-23 20:33

    C++服務編譯耗時優化的原理和服務分析

    ;// ...class Event {// 業務A, B, C ...shared_ptr a1;shared_ptr a2;// ...shared_ptr b1;
    發表于 12-23 17:32

    如何檢測并強化對侵入篡改的攻擊

    相對來說容易找到。有多種方法可以篡改儀表。除侵入篡改方法外,還可在不打開儀表外殼的情況下侵入地篡改電子儀表。磁性篡改是非
    發表于 11-10 07:46

    RT-Thread中侵入鏈表的應用有哪些呢

    侵入鏈表普通鏈表學習數據結構的時候寫的鏈表是下面這個樣子的typedef struct LNode{int data;/* 數據域 /int num; / 數據域
    發表于 12-05 13:59

    如何在C++中包含C代碼?

    :32: 未定義對“CB_Display_GetValues”的引用你有這個問題的解決方案或者你知道如何解決嗎?在 C 中包含 C++ 代碼如何工作(相反的方式)?非常感謝,節日快樂,
    發表于 01-11 06:42

    空指針引用缺陷分類假陽性識別方法

    針對靜態測試中空指針引用缺陷假陽性問題,提出一種空指針引用缺陷分類假陽性識別方法。挖掘空指針引用缺陷
    發表于 11-25 11:04 ?8次下載
    空指針<b class='flag-5'>引用</b><b class='flag-5'>缺陷</b>分類假陽性識別方法

    C++引用的使用場景

    C++引用的使用場景
    的頭像 發表于 06-29 15:18 ?3857次閱讀
    <b class='flag-5'>C++</b>:<b class='flag-5'>引用</b>的使用場景

    C++中的const和引用的討論

    今天給大家分享一下這段時間學習c++的總結學習:c++里面的const關鍵字和引用。
    的頭像 發表于 12-24 15:35 ?657次閱讀

    C++基礎語法中的引用、封裝和多態

    本期是C++基礎語法分享的第六節,今天給大家來分享一下: (1)引用; (2)宏; (3)成員初始化列表; (4)封裝; (5)繼承; (6)多態; 引用 左值引用 常規
    的頭像 發表于 09-12 09:58 ?1172次閱讀

    C++中的智能指針

    在C++11中,有unique_ptr、shared_ptr以及weak_ptr三種,auto_ptr因為自身轉移所有權的原因,在C++11中被廢棄(本節最后,將簡單說下被廢棄的原因)
    的頭像 發表于 08-05 11:11 ?758次閱讀

    C++引用和指針

    之前的文章我們已經介紹了C++中的基本類型如int,bool和double等,除了基本類型C++還有一些更復雜的數據類型復合類型,所謂的復合類型就是通過其他類型定義的類型,本篇文章我們將會著重介紹C++的復合類型
    的頭像 發表于 03-17 14:00 ?455次閱讀

    C++智能指針的底層實現原理

    C++智能指針的頭文件: #include 1. shared_ptr: 智能指針從本質上來說是一個模板類,用類實現對指針對象的管理。 template class shared_ptr
    的頭像 發表于 11-09 14:32 ?355次閱讀
    <b class='flag-5'>C++</b>智能指針的底層實現原理
    亚洲欧美日韩精品久久_久久精品AⅤ无码中文_日本中文字幕有码在线播放_亚洲视频高清不卡在线观看
    <acronym id="s8ci2"><small id="s8ci2"></small></acronym>
    <rt id="s8ci2"></rt><rt id="s8ci2"><optgroup id="s8ci2"></optgroup></rt>
    <acronym id="s8ci2"></acronym>
    <acronym id="s8ci2"><center id="s8ci2"></center></acronym>