C++ shared_ptr四宗罪(不得不轉(zhuǎn))

本文版權(quán)歸 liancheng 所有燥撞,如有轉(zhuǎn)載請(qǐng)按如下方式標(biāo)明原創(chuàng)作者及出處,以示尊重C灾摹叨吮!
原創(chuàng)作者:liancheng
原文出處:http://blog.liancheng.info/?p=85

問題描述

在基于C++的大型系統(tǒng)的設(shè)計(jì)實(shí)現(xiàn)中,由于缺乏語言級(jí)別的GC支持瞬矩,資源生存周期往往是一個(gè)棘手的問題茶鉴。系統(tǒng)地解決這個(gè)問題的方法無非兩種:

  • 使用GC庫
  • 使用引用計(jì)數(shù)

嚴(yán)格地說,引用計(jì)數(shù)其實(shí)也是一種最樸素的GC景用。相對(duì)于現(xiàn)代的GC技術(shù)涵叮,引用計(jì)數(shù)的實(shí)現(xiàn)簡單,但相應(yīng)地伞插,它也存在著循環(huán)引用和線程同步開銷等問題割粮。關(guān)于這二者孰優(yōu)孰劣,已經(jīng)有過很多討論,在此就不攪這股混水了。我一直也沒有使用過C++的GC庫镐侯,在實(shí)際項(xiàng)目中總是采用引用計(jì)數(shù)的方案欠窒。而作為Boost的擁躉伪嫁,首選的自然是shared_ptr。一直以來我也對(duì)shared_ptr百般推崇,然而最近的一些項(xiàng)目開發(fā)經(jīng)驗(yàn)卻讓我在shared_ptr上栽了坑,對(duì)C++引用計(jì)數(shù)也有了一些新的的認(rèn)識(shí)堰怨,遂記錄在此。

本文主要針對(duì)基于boost::shared_ptr的C++引用計(jì)數(shù)實(shí)現(xiàn)方案進(jìn)行一些討論蛇摸。 C++引用計(jì)數(shù)方案往往伴隨著用于自動(dòng)管理引用計(jì)數(shù)的智能指針备图。按是否要求資源對(duì)象自己維護(hù)引用計(jì)數(shù),C++引用計(jì)數(shù)方案可以分為兩類:

  • 侵入式:侵入式的引用計(jì)數(shù)管理要求資源對(duì)象本身維護(hù)引用計(jì)數(shù)赶袄,同時(shí)提供增減引用計(jì)數(shù)的管理接口揽涮。通常侵入式方案會(huì)提供配套的侵入式引用計(jì)數(shù)智能指針。該智能指針通過調(diào)用資源對(duì)象的引用計(jì)數(shù)管理接口來自動(dòng)增減引用計(jì)數(shù)饿肺。COM對(duì)象與CComPtr便是侵入式引用計(jì)數(shù)的一個(gè)典型實(shí)例蒋困。
  • 非侵入式:非侵入式的引用計(jì)數(shù)管理對(duì)資源對(duì)象本身沒有任何要求,而是完全借助非侵入式引用計(jì)數(shù)智能指針在資源對(duì)象外部維護(hù)獨(dú)立的引用計(jì)數(shù)唬格。shared_ptr便是基于這個(gè)思路家破。

第一宗罪

初看起來颜说,非侵入式方案由于對(duì)資源對(duì)象的實(shí)現(xiàn)沒有任何要求购岗,相較于侵入式方案更具吸引力汰聋。然而事實(shí)卻并非如此。下面就來分析一下基于shared_ptr的非侵入式引用計(jì)數(shù)喊积。
在使用shared_ptr的引用計(jì)數(shù)解決方案中烹困,引用計(jì)數(shù)完全由shared_ptr控制,資源對(duì)象對(duì)與自己對(duì)應(yīng)的引用計(jì)數(shù)一無所知乾吻。而引用計(jì)數(shù)與資源對(duì)象的生存期息息相關(guān)髓梅,這就意味著資源對(duì)象喪失了對(duì)生存期的控制權(quán),將自己的生殺大權(quán)拱手讓給了shared_ptr绎签。這種情況下枯饿,資源對(duì)象就不得不依靠至少一個(gè)shared_ptr實(shí)例來保障自己的生存。
換言之诡必,資源對(duì)象一旦“沾染”了shared_ptr奢方,就一輩子都無法擺脫! 考察以下的簡單用例:

用例一:

Resource* p = new CResource;  
{  
    shared_ptr q(p);  
}  
p->Use() // CRASH   

單純?yōu)榱私鉀Q上述的崩潰爸舒,可以自定義一個(gè)什么也不做的deleter:

 struct noop_deleter {  
    void operator()(void*) {  
        // NO-OP  
    }  
};  

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

shared_ptr q(p, noop_deleter());

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

從這個(gè)簡單的用例可以看出忠聚,shared_ptr就像是毒品一樣,一旦沾染就難以戒除唱捣。更甚者咒林,染毒者連換用其他“毒品”的權(quán)力都沒有:shared_ptr的引用計(jì)數(shù)管理接口是私有的,無法從shared_ptr之外操控爷光,也就無法從shared_ptr遷移到其他類型的引用計(jì)數(shù)智能指針垫竞。

不僅如此,資源對(duì)象沾染上shared_ptr之后蛀序,就只能使用最初的那個(gè)shared_ptr實(shí)例的拷貝來維系自己的生存期欢瞪。考察以下用例:
用例二:

 {  
    shared_ptr p1(new CResource);  
    shared_ptr p2(p1);            // OK  
    CResource* p3 = p1.get();  
    shared_ptr p4(p3);            // ERROR  
                                  // CRASH  
}   

該用例的執(zhí)行過程如下:

  1. p1在構(gòu)造的同時(shí)為資源對(duì)象創(chuàng)建了一份外部引用計(jì)數(shù)徐裸,并將之置為1
  2. p2拷貝自p1遣鼓,與p1共享同一個(gè)引用計(jì)數(shù),將之增加為2
  3. p4并非p1的拷貝重贺,因此在構(gòu)造的同時(shí)又為資源對(duì)象創(chuàng)建了另外一個(gè)外部引用計(jì)數(shù)骑祟,并將之置為1
  4. 在作用域結(jié)束時(shí)回懦,p4析構(gòu),由其維護(hù)的額外的引用計(jì)數(shù)降為0次企,導(dǎo)致資源對(duì)象被析構(gòu)
  5. 然后p2析構(gòu)怯晕,對(duì)應(yīng)的引用計(jì)數(shù)降為1
  6. 接著p1析構(gòu),對(duì)應(yīng)的引用計(jì)數(shù)也歸零缸棵,于是p1在臨死之前再次釋放資源對(duì)象
    最后舟茶,由于資源對(duì)象被二次釋放,程序崩潰

至此堵第,我們已經(jīng)認(rèn)識(shí)到了shared_ptr的第一宗罪——傳播毒品

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

第二宗罪

乘勝追擊,再揭露一下shared_ptr的第二宗罪——散布病毒踏志。有點(diǎn)聳人聽聞了阀捅?其實(shí)道理很簡單:由于使用了shared_ptr的資源對(duì)象必須仰仗shared_ptr的存在才能維系生存期,這就意味著使用資源的客戶對(duì)象也必須使用shared_ptr來持有資源對(duì)象的引用——于是shared_ptr的勢力范圍成功地從資源對(duì)象本身擴(kuò)散到了資源使用者针余,侵入了資源客戶對(duì)象的實(shí)現(xiàn)饲鄙。同時(shí),資源的使用者往往是通過某種形式的資源分配器來獲取資源涵紊。自然地傍妒,為了向客戶轉(zhuǎn)交資源對(duì)象的所有權(quán),資源分配器也不得不在接口中傳遞shared_ptr摸柄,于是shared_ptr也會(huì)侵入資源分配器的接口颤练。

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

shared_ptr AllocateResource() {  
    shared_ptr pResource(new CResource);  
    InitResource(pResource.get());  
    return pResource;  
}     
void InitResource(IResource* r) {  
    // Do resource initialization...  
}

以上用例中驱负,在InitResource的執(zhí)行期間嗦玖,由于AllocateResource的堆棧仍然存在,pResource不會(huì)析構(gòu)跃脊,因此可以放心的在InitResource的參數(shù)中使用裸指針傳遞資源對(duì)象宇挫。這種基于調(diào)用棧的引用計(jì)數(shù)優(yōu)化,也是一種常用的手段酪术。但在InitResource返回后器瘪,資源對(duì)象終究還是會(huì)落入shared_ptr的魔掌。
由此可以看出绘雁,shared_ptr打著“非侵入式”的幌子橡疼,雖然沒有侵入資源對(duì)象的實(shí)現(xiàn),卻侵入了資源分配接口以及資源客戶對(duì)象的實(shí)現(xiàn)庐舟。而沾染上shared_ptr就擺脫不掉欣除,如此傳播下去,簡直就是侵入了除資源對(duì)象實(shí)現(xiàn)以外的其他各個(gè)地方挪略!這不是病毒是什么历帚?

然而滔岳,基于shared_ptr的引用計(jì)數(shù)解決方案真的不會(huì)侵入資源對(duì)象的實(shí)現(xiàn)嗎?

第三宗罪

在一些用例中挽牢,資源對(duì)象的成員方法(不包括構(gòu)造函數(shù))需要獲取指向?qū)ο笞陨砥酌海窗藅his指針的shared_ptr。Boost.Asio的chat示例便展示了這樣一個(gè)用例:chat_session對(duì)象會(huì)在其成員函數(shù)中發(fā)起異步I/O操作卓研,并在異步I/O操作回調(diào)中保存一個(gè)指向自己的shared_ptr以保證回調(diào)執(zhí)行時(shí)自身的生存期尚未結(jié)束趴俘。這種手法在Boost.Asio中非常常見睹簇,在不考慮shared_ptr帶來的麻煩時(shí)奏赘,這實(shí)際上也是一種相當(dāng)優(yōu)雅的異步流程資源生存期處理方法。但現(xiàn)在讓我們把注意力集中在shared_ptr上太惠。

通常磨淌,使用shared_ptr的資源對(duì)象必須動(dòng)態(tài)分配,最常見的就是直接從堆上new出一個(gè)實(shí)例并交付給一個(gè)shared_ptr凿渊,或者也可以從某個(gè)資源池中分配再借助自定義的deleter在引用計(jì)數(shù)歸零時(shí)將資源放回池中梁只。無論是那種用法,該資源對(duì)象的實(shí)例在創(chuàng)建出來后埃脏,都總是立即交付給一個(gè)shared_ptr(記為p)搪锣。有鑒于之前提到的毒性三,如果資源對(duì)象的成員方法需要獲取一個(gè)指向自己的shared_ptr彩掐,那么這個(gè)shared_ptr也必須是p的一個(gè)拷貝——或者更本質(zhì)的說构舟,必須與p共享同一個(gè)外部引用計(jì)數(shù)。然而對(duì)于資源對(duì)象而言堵幽,p維護(hù)的引用計(jì)數(shù)是外部的陌生事物狗超,資源對(duì)象如何得到這個(gè)引用計(jì)數(shù)并由此構(gòu)造出一個(gè)合法的shared_ptr呢?這是一個(gè)比較tricky的過程朴下。為了解決這個(gè)問題努咐,Boost提供了一個(gè)類模板e(cuò)nable_shared_from_this:

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

class CResource :  
    public boost::enable_shared_from_this<CResource>  
{  
    // ...  
};  

接著渗稍,資源對(duì)象的成員方法就可以使用enable_shared_from_this::shared_from_this()方法來獲取所需的指向?qū)ο笞陨淼膕hared_ptr了。問題似乎解決了团滥。但是竿屹,等等!這樣的繼承體系不就對(duì)資源對(duì)象的實(shí)現(xiàn)有要求了嗎惫撰?換言之羔沙,這不正是對(duì)資源對(duì)象實(shí)現(xiàn)的赤裸裸的侵入嗎?這正是shared_ptr的第三宗罪——欺世盜名厨钻。

第四宗罪

最后一宗罪扼雏,是鋪張浪費(fèi)坚嗜。對(duì)了,說的就是性能诗充。

基于引用計(jì)數(shù)的資源生存期管理苍蔬,打一出生起就被扣著線程同步開銷大的帽子。早期的Boost版本中蝴蜓,shared_ptr是借助Boost.Thread的mutex對(duì)象來保護(hù)引用計(jì)數(shù)碟绑。在后期的版本中采用了lock-free的原子整數(shù)操作一定程度上降低了線程同步開銷。然而即使是lock-free茎匠,本質(zhì)上也仍然是串行化訪問格仲,線程同步的開銷多少都會(huì)存在。也許有人會(huì)說這點(diǎn)開銷與引用計(jì)數(shù)帶來的便利相比算不得什么诵冒。然而在我們項(xiàng)目的異步服務(wù)器框架的壓力測試中凯肋,大量引用計(jì)數(shù)的增減操作,一舉吃掉了5%的CPU汽馋。換言之侮东,1/20的計(jì)算能力被浪費(fèi)在了與業(yè)務(wù)邏輯完全無關(guān)的引用計(jì)數(shù)的維護(hù)上!而且豹芯,由于是異步流程的特殊性悄雅,也無法應(yīng)用上面提及的基于調(diào)用棧的引用計(jì)數(shù)優(yōu)化。

那么針對(duì)這個(gè)問題就真的沒有辦法了嗎铁蹈?其實(shí)仔細(xì)檢視一下整個(gè)異步流程宽闲,有些資源雖然會(huì)先后被不同的對(duì)象所引用,但在其整個(gè)生存周期內(nèi)木缝,每一時(shí)刻都只有一個(gè)對(duì)象持有該資源的引用便锨。用于數(shù)據(jù)收發(fā)的緩沖區(qū)對(duì)象就是一個(gè)典型。它們總是被從某個(gè)源頭產(chǎn)生我碟,然后便一直從一處被傳遞到另一處放案,最終在某個(gè)時(shí)刻被回收。對(duì)于這樣的對(duì)象矫俺,實(shí)際上沒有必要針對(duì)流程中的每一次所有權(quán)轉(zhuǎn)移都進(jìn)行引用計(jì)數(shù)操作吱殉,只要簡單地在分配時(shí)將引用計(jì)數(shù)置1,在需要釋放時(shí)再將引用計(jì)數(shù)歸零便可以了厘托。

對(duì)于侵入式引用計(jì)數(shù)方案友雳,由于資源對(duì)象自身持有引用計(jì)數(shù)并提供了引用計(jì)數(shù)的操作接口,可以很容易地實(shí)現(xiàn)這樣的優(yōu)化铅匹。但shared_ptr則不然押赊。shared_ptr把引用計(jì)數(shù)牢牢地攥在手中,不讓外界碰觸包斑;外界只有通過shared_ptr的構(gòu)造函數(shù)流礁、析夠函數(shù)以及reset()方法才能夠間接地對(duì)引用計(jì)數(shù)進(jìn)行操作涕俗。而由于shared_ptr的毒品特性,資源對(duì)象無法脫離shared_ptr而存在神帅,因此在轉(zhuǎn)移資源對(duì)象的所有權(quán)時(shí)再姑,也必須通過拷貝shared_ptr的方式進(jìn)行。一次拷貝就對(duì)應(yīng)一對(duì)引用計(jì)數(shù)的原子增減操作找御。對(duì)于上述的可優(yōu)化資源對(duì)象元镀,如果在一個(gè)流程中被傳遞3次,除去分配和釋放時(shí)的2次霎桅,還會(huì)導(dǎo)致6次無謂的原子整數(shù)操作栖疑。整整浪費(fèi)了300%!

事實(shí)證明哆档,在將基于shared_ptr的非侵入式引用計(jì)數(shù)方案更改為侵入式引用計(jì)數(shù)方案并施行上述優(yōu)化后蔽挠,我們的異步服務(wù)器框架的性能有了明顯的提升住闯。

總結(jié)

最后總結(jié)一下shared_ptr的四宗罪:

  • 傳播毒品
    一旦對(duì)資源對(duì)象染上了shared_ptr瓜浸,在其生存期內(nèi)便無法擺脫。
  • 散布病毒
    在應(yīng)用了shared_ptr的資源對(duì)象的所有權(quán)變換的整個(gè)過程中的所有接口都會(huì)受到shared_ptr的污染比原。
  • 欺世盜名
    在enable_shared_from_this用例下插佛,基于shared_ptr的解決方案并非是非侵入式的。
  • 鋪張浪費(fèi)
    由于shared_ptr隱藏了引用計(jì)數(shù)的操作接口量窘,只能通過拷貝shared_ptr的方式間接操縱引用計(jì)數(shù)雇寇,使得用戶難以規(guī)避不必要的引用計(jì)數(shù)操作,造成無謂的性能損失蚌铜。

探明這四宗罪算是最近一段時(shí)間的項(xiàng)目設(shè)計(jì)開發(fā)過程的一大收獲锨侯。寫這篇文章的目的不是為了將shared_ptr一棒子打死,只是為了總結(jié)基于shared_ptr的C++非侵入式引用計(jì)數(shù)解決方案的缺陷冬殃,也讓自己不再盲目迷信shared_ptr囚痴。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市审葬,隨后出現(xiàn)的幾起案子深滚,更是在濱河造成了極大的恐慌,老刑警劉巖涣觉,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痴荐,死亡現(xiàn)場離奇詭異,居然都是意外死亡官册,警方通過查閱死者的電腦和手機(jī)生兆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來膝宁,“玉大人鸦难,你說我怎么就攤上這事栖榨。” “怎么了明刷?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵婴栽,是天一觀的道長。 經(jīng)常有香客問我辈末,道長愚争,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任挤聘,我火速辦了婚禮轰枝,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘组去。我一直安慰自己鞍陨,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布从隆。 她就那樣靜靜地躺著诚撵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪键闺。 梳的紋絲不亂的頭發(fā)上寿烟,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音辛燥,去河邊找鬼筛武。 笑死,一個(gè)胖子當(dāng)著我的面吹牛挎塌,可吹牛的內(nèi)容都是我干的徘六。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼榴都,長吁一口氣:“原來是場噩夢啊……” “哼待锈!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起缭贡,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤炉擅,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后阳惹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谍失,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年莹汤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了快鱼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抹竹,靈堂內(nèi)的尸體忽然破棺而出线罕,到底是詐尸還是另有隱情,我是刑警寧澤窃判,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布钞楼,位于F島的核電站,受9級(jí)特大地震影響袄琳,放射性物質(zhì)發(fā)生泄漏询件。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一唆樊、第九天 我趴在偏房一處隱蔽的房頂上張望宛琅。 院中可真熱鬧,春花似錦逗旁、人聲如沸嘿辟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽红伦。三九已至,卻和暖如春堤舒,著一層夾襖步出監(jiān)牢的瞬間色建,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國打工舌缤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人某残。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓国撵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親玻墅。 傳聞我的和親對(duì)象是個(gè)殘疾皇子介牙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • C++ 智能指針詳解 一、簡介由于 C++ 語言沒有自動(dòng)內(nèi)存回收機(jī)制澳厢,程序員每次 new 出來的內(nèi)存都要手動(dòng) de...
    yangqi916閱讀 1,370評(píng)論 0 2
  • 原作者:Babu_Abdulsalam 本文翻譯自CodeProject环础,轉(zhuǎn)載請(qǐng)注明出處。 引入### Ooops...
    卡巴拉的樹閱讀 30,099評(píng)論 13 74
  • 轉(zhuǎn)自http://blog.csdn.net/xugangwen/article/details/44811783...
    扎Zn了老Fe閱讀 12,729評(píng)論 1 142
  • 1. 什么是智能指針剩拢? 智能指針是行為類似于指針的類對(duì)象线得,但這種對(duì)象還有其他功能。 2. 為什么設(shè)計(jì)智能指針徐伐? 引...
    MinoyJet閱讀 638評(píng)論 0 1
  • 1. 讓自己習(xí)慣C++ 條款01:視C++為一個(gè)語言聯(lián)邦 為了更好的理解C++贯钩,我們將C++分解為四個(gè)主要次語言:...
    Mr希靈閱讀 2,809評(píng)論 0 13