shared_ptr線程安全性全面分析
正如《STL源碼剖析》所講,“源碼之前,了無秘密”。本文基于shared_ptr的源代碼,提取了shared_ptr的類圖和對象圖,然后分析了shared_ptr如何保證文檔所宣稱的線程安全性。本文的分析基于boost 1.52版本,編譯器是VC 2010。
shared_ptr的線程安全性
boost官方文檔對shared_ptr線程安全性的正式表述是:shared_ptr對象提供與內(nèi)置類型相同級別的線程安全性?!緎hared_ptrobjects offer the same level of thread safety as built-in types.】具體是以下三點(diǎn)。
1. 同一個shared_ptr對象可以被多線程同時讀取。【A shared_ptrinstance can be "read" (accessed using only const operations)simultaneously by multiple threads.】
2. 不同的shared_ptr對象可以被多線程同時修改(即使這些shared_ptr對象管理著同一個對象的指針)。【Different shared_ptr instances can be "written to"(accessed using mutable operations such as operator= or reset) simultaneouslyby multiple threads (even when these instances are copies, and share the samereference count underneath.) 】
3. 任何其他并發(fā)訪問的結(jié)果都是無定義的。【Any other simultaneous accesses result in undefined behavior.】
第一種情況是對對象的并發(fā)讀,自然是線程安全的。
第二種情況下,如果兩個shared_ptr對象A和B管理的是不同對象的指針,則這兩個對象完全不相關(guān),支持并發(fā)寫也容易理解。但如果A和B管理的是同一個對象P的指針,則A和B需要維護(hù)一塊共享的內(nèi)存區(qū)域,該區(qū)域記錄P指針當(dāng)前的引用計(jì)數(shù)。對A和B的并發(fā)寫必然涉及對該引用計(jì)數(shù)內(nèi)存區(qū)的并發(fā)修改,這需要boost做額外的工作,也是本文分析的重點(diǎn)。
另外weak_ptr和shared_ptr緊密相關(guān),用戶可以從weak_ptr構(gòu)造出shared_ptr,也可以從shared_ptr構(gòu)造weak_ptr,但是weak_ptr不涉及到對象的生命周期。由于shared_ptr的線程安全性是和weak_ptr耦合在一起的,本文的分析也涉及到weak_ptr。
下面先從總體上看一下shared_ptr和weak_ptr的實(shí)現(xiàn)。
shared_ptr的結(jié)構(gòu)圖
以下是從boost源碼提取出的shared_ptr和weak_ptr的類圖。

我們首先忽略虛線框內(nèi)的weak_ptr部分。最高層的shared_ptr就是用戶直接使用的類,它提供shared_ptr的構(gòu)造、復(fù)制、重置(reset函數(shù))、解引用、比較、隱式轉(zhuǎn)換為bool等功能。它包含一個指向被管理對象的指針,用來實(shí)現(xiàn)解引用操作,并且組合了一個shared_count對象,用來操作引用計(jì)數(shù)。
但shared_count類還不是引用計(jì)數(shù)類,它只是包含了一個指向引用計(jì)數(shù)類sp_counted_base的指針,功能上是對sp_counted_base操作的封裝。shared_count對象的創(chuàng)建、復(fù)制和刪除等操作,包含著對sp_counted_base的增加和減小引用計(jì)數(shù)的操作。
最后sp_counted_base類才保存了引用計(jì)數(shù),并且對引用計(jì)數(shù)字段提供無鎖保護(hù)。它也包含了一個指向被管理對象的指針,是用來刪除被管理的對象的。sp_counted_base有三個派生類,分別處理用戶指定Deleter和Allocator的情況:
1. sp_counted_impl_p:用戶沒有指定Deleter和Allocator
2. sp_counted_impl_pd:用戶指定了Deleter,沒有指定Allocator
3. sp_counted_impl_pda:用戶指定了Deleter和 Allocator
創(chuàng)建指針P的第一個shared_ptr對象的時候,子對象shared_count同時被建立, shared_count根據(jù)用戶提供的參數(shù)選擇創(chuàng)建一個特定的sp_counted_base派生類對象X。之后創(chuàng)建的所有管理P的shared_ptr對象都指向了這個獨(dú)一無二的X。
然后再看虛線框內(nèi)的weak_ptr就清楚了。weak_ptr和shared_ptr基本上類似,只不過weak_ptr包含的是weak_count子對象,但weak_count和shared_count也都指向了sp_counted_base。
如果上面的文字還不夠清楚,下面的代碼就能說明問題。
shared_ptr<SomeObject> SP1(new SomeObject());
shared_ptr<SomeObject> SP2=SP1;
weak_ptr<SomeObject> WP1=SP1;
執(zhí)行完以上代碼后,內(nèi)存中會創(chuàng)建以下對象實(shí)例,其中紅色箭頭表示指向引用計(jì)數(shù)對象的指針,黑色箭頭表示指向被管理對象的指針。

從上面可以清楚的看出,SP1、SP2和WP1指向了同一個sp_counted_impl_p對象,這個sp_counted_impl_p對象保存引用計(jì)數(shù),是SP1、SP2和WP1等三個對象共同操作的內(nèi)存區(qū)。多線程并發(fā)修改SP1、SP2和WP1,有且只有sp_counted_impl_p對象會被并發(fā)修改,因此sp_counted_impl_p的線程安全性是shared_ptr以及weak_ptr線程安全性的關(guān)鍵問題。而sp_counted_impl_p的線程安全性是在其基類sp_counted_base中實(shí)現(xiàn)的。下面將著重分析sp_counted_base的代碼。
引用計(jì)數(shù)類sp_counted_base
幸運(yùn)的是,sp_counted_base的代碼量很小,下面全文列出來,并添加有注釋。
class sp_counted_base
{
private:
// 禁止復(fù)制
sp_counted_base( sp_counted_base const & );
sp_counted_base & operator= ( sp_counted_baseconst & );
// shared_ptr的數(shù)量
long use_count_;
// weak_ptr的數(shù)量+1
long weak_count_;
public:
// 唯一的一個構(gòu)造函數(shù),注意這里把兩個計(jì)數(shù)都置為1
sp_counted_base(): use_count_( 1 ), weak_count_( 1 ){ }
// 虛基類,因此可以作為基類
virtual ~sp_counted_base(){ }
// 子類需要重載,用operator delete或者Deleter刪除被管理的對象
virtual void dispose() = 0;
// 子類可以重載,用Allocator等刪除當(dāng)前對象
virtual void destroy(){
delete this;
}
virtual void * get_deleter( sp_typeinfo const & ti ) = 0;
// 這個函數(shù)在根據(jù)shared_count復(fù)制shared_count的時候用到
// 既然存在一個shared_count作為源,記為A,則只要A不釋放,
// use_count_就不會被另一個線程release()為1。
// 另外,如果一個線程把A作為復(fù)制源,另一個線程釋放A,執(zhí)行結(jié)果是未定義的。
void add_ref_copy(){
_InterlockedIncrement( &use_count_ );
}
// 這個函數(shù)在根據(jù)weak_count構(gòu)造shared_count的時候用到
// 這是為了避免通過weak_count增加引用計(jì)數(shù)的時候,
// 另外的線程卻調(diào)用了release函數(shù),清零use_count_并釋放了指向的對象
bool add_ref_lock(){
for( ;; )
{
long tmp = static_cast< long const volatile& >( use_count_ );
if( tmp == 0 ) return false;
if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;
}
}
void release(){
if( _InterlockedDecrement( &use_count_ ) == 0 )
{
// use_count_從1變成0的時候,
// 1. 釋放對象
// 2. 對weak_count_執(zhí)行一次遞減操作。這是因?yàn)樵诔跏蓟臅r候(use_count_從0變1時),weak_count初始值為1
dispose();
weak_release();
}
}
void weak_add_ref(){
_InterlockedIncrement( &weak_count_ );
}
// 遞減weak_count_;且在weak_count為0的時候,把自己刪除
void weak_release(){
if( _InterlockedDecrement( &weak_count_ ) == 0 )
{
destroy();
}
}
// 返回引用計(jì)數(shù)。注意如果用戶沒有額外加鎖,引用計(jì)數(shù)完全可能同時被另外的線程修改掉。
long use_count() const{
return static_cast<long const volatile &>( use_count_ );
}
};
代碼中的注釋已經(jīng)說明了一些問題,這里再重復(fù)一點(diǎn):use_count_字段等于當(dāng)前shared_ptr對象的數(shù)量,weak_count_字段等于當(dāng)前weak_ptr對象的數(shù)量加1。
首先不考慮weak_ptr的情況。根據(jù)對shared_ptr類的代碼分析(代碼沒有列出來,但很容易找到),shared_ptr之間的復(fù)制都是調(diào)用add_ref_copy和release函數(shù)進(jìn)行的。假設(shè)兩個線程分別對SP1和SP2進(jìn)行操作,操作的過程無非是以下三種情況:
1. SP1和SP2都遞增引用計(jì)數(shù),即add_ref_copy被并發(fā)調(diào)用,也就是兩個_InterlockedIncrement(&use_count_)并發(fā)執(zhí)行,這是線程安全的。
2. SP1和SP2都遞減引用計(jì)數(shù),即release被并發(fā)調(diào)用,也就是_InterlockedDecrement(&use_count_ )并發(fā)執(zhí)行,這也是線程安全的。只不過后執(zhí)行的線程負(fù)責(zé)刪除對象。
3. SP1遞增引用計(jì)數(shù),調(diào)用add_ref_copy;SP2遞減引用計(jì)數(shù),調(diào)用release。由于SP1的存在,SP2的release操作無論如何都不會導(dǎo)致use_count_變?yōu)榱悖簿褪钦frelease中if語句的body永遠(yuǎn)不會被執(zhí)行。因此,這種情況就化簡為_InterlockedIncrement(&use_count_)和_InterlockedDecrement( &use_count_ )的并發(fā)執(zhí)行,仍然是線程安全的。
然后考慮weak_ptr。如果是weak_ptr之間的操作,或者從shared_ptr構(gòu)造weak_ptr,都不涉及到use_count_的操作,只需要調(diào)用weak_add_ref和weak_release來操作weak_count_。與上面的分析相同,_InterlockedIncrement和_InterlockedDecrement保證了weak_add_ref和weak_release并發(fā)操作的線程安全性。但如果存在從weak_ptr構(gòu)造shared_ptr的操作,則需要考慮在構(gòu)造weak_ptr的過程中,被管理的對象已經(jīng)被其他線程被釋放的情況。如果從weak_ptr構(gòu)造shared_ptr仍然是通過add_ref_copy函數(shù)完成的,則可能發(fā)生以下錯誤情況:
|
線程1,從weak_ptr創(chuàng)建shared_ptr |
線程2,釋放目前唯一存在的shared_ptr |
1 |
判斷use_count_大于0,等待執(zhí)行add_ref_copy |
|
2 |
|
調(diào)用release,use_count--。發(fā)現(xiàn)use_count為0,刪除被管理的對象 |
3 |
開始執(zhí)行add_ref_copy,導(dǎo)致 use_count遞增。 發(fā)生錯誤,use_count==1,但是對象已經(jīng)被刪除了 |
|
我們自然會想,線程1在第三行結(jié)束后,再判斷一次use_count是否為1,如果是1,認(rèn)為對象已經(jīng)刪除,判斷失敗不就可以了嗎。其實(shí)是行不通的,下面是一個反例。
|
線程1,從weak_ptr創(chuàng)建shared_ptr |
線程2,釋放目前唯一存在的shared_ptr |
線程3,從weak_ptr創(chuàng)建shared_ptr |
1 |
判斷use_count_大于0,等待執(zhí)行add_ref_copy |
|
|
2 |
|
|
判斷use_count_大于0,等待執(zhí)行add_ref_copy |
3 |
|
調(diào)用release,use_count--。發(fā)現(xiàn)use_count為0,刪除被管理的對象 |
|
4 |
開始執(zhí)行add_ref_copy,導(dǎo)致 use_count遞增。 |
|
|
5 |
|
|
執(zhí)行add_ref_copy,導(dǎo)致 use_count遞增。 |
6 |
發(fā)現(xiàn)use_count_ != 1,判斷執(zhí)行成功。 發(fā)生錯誤,use_count==2,但是對象已經(jīng)被刪除了 |
|
發(fā)現(xiàn)use_count_ != 1,判斷執(zhí)行成功。 發(fā)生錯誤,use_count==2,但是對象已經(jīng)被刪除了 |
實(shí)際上,boost從weak_ptr構(gòu)造shared_ptr不是調(diào)用add_ref_copy,而是調(diào)用add_ref_lock函數(shù)。add_ref_lock是典型的無鎖修改共享變量的代碼,下面再把它的代碼復(fù)制一遍,并添加證明注釋。
bool add_ref_lock(){
for( ;; )
{
// 第一步,記錄下use_count_
long tmp = static_cast< long const volatile& >( use_count_ );
// 第二步,如果已經(jīng)被別的線程搶先清0了,則被管理的對象已經(jīng)或者將要被釋放,返回false
if( tmp == 0 ) return false;
// 第三步,如果if條件執(zhí)行成功,
// 說明在修改use_count_之前,use_count仍然是tmp,大于0
// 也就是說use_count_在第一步和第三步之間,從來沒有變?yōu)?過。
// 這是因?yàn)閡se_count一旦變?yōu)?,就不可能再次累加為大于0
// 因此,第一步和第三步之間,被管理的對象不可能被釋放,返回true。
if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;
}
}
在上面的注釋中,用到了一個沒有被證明的結(jié)論,“use_count一旦變?yōu)?,就不可能再次累加為大于0”。下面四條可以證明它。
1.use_count_是sp_counted_base類的private對象,sp_counted_base也沒有友元函數(shù),因此use_count_不會被對象外的代碼修改。
2.成員函數(shù)add_ref_copy可以遞增use_count_,但是所有對add_ref_copy函數(shù)的調(diào)用都是通過一個shared_ptr對象執(zhí)行的。既然存在shared_ptr對象,use_count在遞增之前一定不是0。
3.成員函數(shù)add_ref_lock可以遞增use_count_,但正如add_ref_lock代碼所示,執(zhí)行第三步的時候,tmp都是大于0的,因此add_ref_lock不會使use_count_從0遞增到1
4.其它成員函數(shù)從來不會遞增use_count_
至此,我們可以放下心來,只要add_ref_lock返回true,遞增引用計(jì)數(shù)的行為就是成功的。因此從weak_ptr構(gòu)造shared_ptr的行為也是完全確定的,要么add_ref_lock返回true,構(gòu)造成功,要么add_ref_lock返回false,構(gòu)造失敗。
綜上所述,多線程通過不同的shared_ptr或者weak_ptr對象并發(fā)修改同一個引用計(jì)數(shù)對象sp_counted_base是線程安全的。而sp_counted_base對象是這些智能指針唯一操作的共享內(nèi)存區(qū),因此最終的結(jié)果就是線程安全的。
其它操作
前面我們分析了,不同的shared_ptr對象可以被多線程同時修改。那其它的問題呢,同一個shared_ptr對象可以對多線程同時修改嗎?我們必須要注意到,前面所有的同步都是針對引用計(jì)數(shù)類sp_counted_base進(jìn)行的,shared_ptr本身并沒有任何同步保護(hù)。我們看下面boost文檔舉出來的非線程安全的例子
// thread A
p3.reset(new int(1));
// thread B
p3.reset(new int(2)); // undefined, multiple writes
下面是shared_ptr類相關(guān)的代碼
template<class Y>
void reset(Y * p)
{
this_type(p).swap(*this);
}
void swap(shared_ptr<T> & other)
{
std::swap(px, other.px);
pn.swap(other.pn);
}
可以看到,reset執(zhí)行了兩個修改成員變量的操作,thread A和thread B的執(zhí)行結(jié)果可能是非法的。。
但是仿照內(nèi)置對象的語義,boost提供了若干個原子函數(shù),支持通過這些函數(shù)并發(fā)修改同一個shared_ptr對象。這包括atomic_store、atomic_exchange、atomic_compare_exchange等。以下是實(shí)現(xiàn)的代碼,不再詳細(xì)分析。
template<class T>
void atomic_store( shared_ptr<T> * p, shared_ptr<T> r ){
boost::detail::spinlock_pool<2>::scoped_lock lock( p );
p->swap( r );
}
template<class T>
shared_ptr<T> atomic_exchange( shared_ptr<T> * p, shared_ptr<T> r ){
boost::detail::spinlock & sp = boost::detail::spinlock_pool<2>::spinlock_for( p );
sp.lock();
p->swap( r );
sp.unlock();
return r;
}
template<class T>
bool atomic_compare_exchange( shared_ptr<T> * p, shared_ptr<T> * v, shared_ptr<T> w ){
boost::detail::spinlock & sp = boost::detail::spinlock_pool<2>::spinlock_for( p );
sp.lock();
if( p->_internal_equiv( *v ) ){
p->swap( w );
sp.unlock();
return true;
}
else{
shared_ptr<T> tmp( *p );
sp.unlock();
tmp.swap( *v );
return false;
}
}
總結(jié)
正如boost文檔所宣稱的,boost為shared_ptr提供了與內(nèi)置類型同級別的線程安全性。這包括:
1. 同一個shared_ptr對象可以被多線程同時讀取。
2. 不同的shared_ptr對象可以被多線程同時修改。
3. 同一個shared_ptr對象不能被多線程直接修改,但可以通過原子函數(shù)完成。
如果把上面的表述中的"shared_ptr"替換為“內(nèi)置類型”也完全成立。
最后,整理這個東西的時候我也發(fā)現(xiàn)有些關(guān)鍵點(diǎn)很難表述清楚,這也是由于線程安全性本身比較難嚴(yán)格證明。如果想要完全理解,還是建議閱讀shared_ptr完整的代碼。
相關(guān)文章
C++實(shí)現(xiàn)圖的鄰接矩陣存儲和廣度、深度優(yōu)先遍歷實(shí)例分析
這篇文章主要介紹了C++實(shí)現(xiàn)圖的鄰接矩陣存儲和廣度、深度優(yōu)先遍歷,實(shí)例分析了C++實(shí)現(xiàn)圖的遍歷技巧,非常具有實(shí)用價值,需要的朋友可以參考下2015-04-04C++中malloc與free、new與delete的詳解與應(yīng)用
今天小編就為大家分享一篇關(guān)于C++中malloc與free、new與delete的詳解與應(yīng)用,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2018-12-12C++的STL中accumulate函數(shù)的使用方法
這篇文章主要介紹了C++的STL中accumulate的使用方法,accumulate作用是累加求和即自定義類型數(shù)據(jù)處理,下文具體的操作方法需要的小伙伴可以參考一下2022-03-03解決C語言中使用scanf連續(xù)輸入兩個字符類型的問題
這篇文章主要介紹了解決C語言中使用scanf連續(xù)輸入兩個字符類型的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12詳解C語言中fseek函數(shù)和ftell函數(shù)的使用方法
這篇文章主要介紹了C語言中fseek函數(shù)和ftell函數(shù)的使用方法,兩個函數(shù)分別用于設(shè)置和返回文件指針stream的位置,需要的朋友可以參考下2016-03-03Qt QTreeWidget 樹形結(jié)構(gòu)實(shí)現(xiàn)代碼
Qt中實(shí)現(xiàn)樹形結(jié)構(gòu)可以使用QTreeWidget類,也可以使用QTreeView類,QTreeWidget繼承自QTreeView類,接下來通過本文給大家介紹Qt QTreeWidget 樹形結(jié)構(gòu)實(shí)現(xiàn)代碼,需要的朋友可以參考下2021-11-11