精品国产人成在线_亚洲高清无码在线观看_国产在线视频国产永久2021_国产AV综合第一页一个的一区免费影院黑人_最近中文字幕MV高清在线视频

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實例的拷貝來維系自己的生存期。考察以下用例:用例二:

{
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

    文章

    480

    瀏覽量

    70511
  • C++
    C++
    +關注

    關注

    22

    文章

    2104

    瀏覽量

    73498
  • 計數
    +關注

    關注

    1

    文章

    56

    瀏覽量

    20088

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

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

收藏 人收藏

    評論

    相關推薦

    記一次詭異的內存泄漏

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

    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陷阱與缺陷CC++經典著作)

    電子發燒友網站提供《C陷阱與缺陷CC++經典著作).txt》資料免費下載
    發表于 06-03 16:26 ?0次下載

    基于模板濾波的侵入負荷辨識系統

    針對侵入負荷監測系統在設備投入、復雜性以及擴展性上的缺陷,在詳細分析侵入負荷監測系統工作原
    發表于 02-01 13:51 ?0次下載
    基于模板濾波的<b class='flag-5'>非</b><b class='flag-5'>侵入</b>負荷辨識系統

    侵入自動測試系統的研制

    侵入系統的關鍵技術點。最后結合現場實際運行,提出引入了侵入概念后的安全實施
    發表于 03-29 11:06 ?0次下載

    ABB全新的侵入溫度解決方案榮獲德國創新獎

    這款侵入溫度傳感器的誕生標志著溫度測量新紀元的開啟。
    的頭像 發表于 06-04 09:29 ?3903次閱讀

    C++引用的使用場景

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

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

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

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

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

    C++中的智能指針

    C++11中,有unique_ptrshared_ptr以及weak_ptr三種,auto_ptr因為自身轉移所有權的原因,在
    的頭像 發表于 08-05 11:11 ?885次閱讀

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

    檢測并強化對侵入篡改的攻擊
    發表于 11-01 08:25 ?0次下載
    檢測并強化對<b class='flag-5'>非</b><b class='flag-5'>侵入</b><b class='flag-5'>式</b>篡改的攻擊

    C++引用和指針

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

    HARDWARIO侵入監測-旋轉監測

    電子發燒友網站提供《HARDWARIO侵入監測-旋轉監測.zip》資料免費下載
    發表于 06-16 10:03 ?0次下載
    HARDWARIO<b class='flag-5'>非</b><b class='flag-5'>侵入</b><b class='flag-5'>式</b>監測-旋轉監測

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

    C++智能指針的頭文件: #include 1. shared_ptr: 智能指針從本質上來說是一個模板類,用類實現對指針對象的管理。 template class shared_ptr
    的頭像 發表于 11-09 14:32 ?707次閱讀
    <b class='flag-5'>C++</b>智能指針的底層實現原理