shared_ptr線程平安性周全剖析。本站提示廣大學習愛好者:(shared_ptr線程平安性周全剖析)文章只能為提供參考,不一定能成為您想要的結果。以下是shared_ptr線程平安性周全剖析正文
正如《STL源碼分析》所講,“源碼之前,了無機密”。本文基於shared_ptr的源代碼,提取了shared_ptr的類圖和對象圖,然後剖析了shared_ptr若何包管文檔所傳播鼓吹的線程平安性。本文的剖析基於boost 1.52版本,編譯器是VC 2010。
shared_ptr的線程平安性
boost官方文檔對shared_ptr線程平安性的正式表述是:shared_ptr對象供給與內置類型雷同級其余線程平安性。【shared_ptrobjects offer the same level of thread safety as built-in types.】詳細是以下三點。
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. 任何其他並發拜訪的成果都是無界說的。【Any other simultaneous accesses result in undefined behavior.】
第一種情形是對對象的並發讀,天然是線程平安的。
第二種情形下,假如兩個shared_ptr對象A和B治理的是分歧對象的指針,則這兩個對象完整不相干,支撐並發寫也輕易懂得。但假如A和B治理的是統一個對象P的指針,則A和B須要保護一塊同享的內存區域,該區域記載P指針以後的援用計數。對A和B的並發寫必定觸及對該援用計數內存區的並發修正,這須要boost做額定的任務,也是本文剖析的重點。
別的weak_ptr和shared_ptr慎密相干,用戶可以從weak_ptr結構出shared_ptr,也能夠從shared_ptr結構weak_ptr,然則weak_ptr不觸及到對象的性命周期。因為shared_ptr的線程平安性是和weak_ptr耦合在一路的,本文的剖析也觸及到weak_ptr。
上面先從整體上看一下shared_ptr和weak_ptr的完成。
shared_ptr的構造圖
以下是從boost源碼提掏出的shared_ptr和weak_ptr的類圖。
我們起首疏忽虛線框內的weak_ptr部門。最高層的shared_ptr就是用戶直接應用的類,它供給shared_ptr的結構、復制、重置(reset函數)、解援用、比擬、隱式轉換為bool等功效。它包括一個指向被治理對象的指針,用來完成解援用操作,而且組合了一個shared_count對象,用來操作援用計數。
但shared_count類還不是援用計數類,它只是包括了一個指向援用計數類sp_counted_base的指針,功效上是對sp_counted_base操作的封裝。shared_count對象的創立、復制和刪除等操作,包括著對sp_counted_base的增長和減短序用計數的操作。
最初sp_counted_base類才保留了援用計數,而且對援用計數字段供給無鎖掩護。它也包括了一個指向被治理對象的指針,是用來刪除被治理的對象的。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
創立指針P的第一個shared_ptr對象的時刻,子對象shared_count同時被樹立, shared_count依據用戶供給的參數選擇創立一個特定的sp_counted_base派生類對象X。以後創立的一切治理P的shared_ptr對象都指向了這個舉世無雙的X。
然後再看虛線框內的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;
履行完以上代碼後,內存中會創立以下對象實例,個中白色箭頭表現指向援用計數對象的指針,黑色箭頭表現指向被治理對象的指針。
從下面可以清晰的看出,SP1、SP2和WP1指向了統一個sp_counted_impl_p對象,這個sp_counted_impl_p對象保留援用計數,是SP1、SP2和WP1等三個對象配合操作的內存區。多線程並發修正SP1、SP2和WP1,有且只要sp_counted_impl_p對象會被並發修正,是以sp_counted_impl_p的線程平安性是shared_ptr和weak_ptr線程平安性的症結成績。而sp_counted_impl_p的線程平安性是在其基類sp_counted_base中完成的。上面將側重剖析sp_counted_base的代碼。
援用計數類sp_counted_base
榮幸的是,sp_counted_base的代碼量很小,上面全文列出來,並添加有正文。
class sp_counted_base
{
private:
// 制止復制
sp_counted_base( sp_counted_base const & );
sp_counted_base & operator= ( sp_counted_baseconst & );
// shared_ptr的數目
long use_count_;
// weak_ptr的數目+1
long weak_count_;
public:
// 獨一的一個結構函數,留意這裡把兩個計數都置為1
sp_counted_base(): use_count_( 1 ), weak_count_( 1 ){ }
// 虛基類,是以可以作為基類
virtual ~sp_counted_base(){ }
// 子類須要重載,用operator delete或許Deleter刪除被治理的對象
virtual void dispose() = 0;
// 子類可以重載,用Allocator等刪除以後對象
virtual void destroy(){
delete this;
}
virtual void * get_deleter( sp_typeinfo const & ti ) = 0;
// 這個函數在依據shared_count復制shared_count的時刻用到
// 既然存在一個shared_count作為源,記為A,則只需A不釋放,
// use_count_就不會被另外一個線程release()為1。
// 別的,假如一個線程把A作為復制源,另外一個線程釋放A,履行成果是不決義的。
void add_ref_copy(){
_InterlockedIncrement( &use_count_ );
}
// 這個函數在依據weak_count結構shared_count的時刻用到
// 這是為了不經由過程weak_count增長援用計數的時刻,
// 別的的線程卻挪用了release函數,清零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_履行一次遞加操作。這是由於在初始化的時刻(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();
}
}
// 前往援用計數。留意假如用戶沒有額定加鎖,援用計數完整能夠同時被別的的線程修正失落。
long use_count() const{
return static_cast<long const volatile &>( use_count_ );
}
};
代碼中的正文曾經解釋了一些成績,這裡再反復一點:use_count_字段等於以後shared_ptr對象的數目,weak_count_字段等於以後weak_ptr對象的數目加1。
起首不斟酌weak_ptr的情形。依據對shared_ptr類的代碼剖析(代碼沒有列出來,但很輕易找到),shared_ptr之間的復制都是挪用add_ref_copy和release函數停止的。假定兩個線程分離對SP1和SP2停止操作,操作的進程不過是以下三種情形:
1. SP1和SP2都遞增援用計數,即add_ref_copy被並發挪用,也就是兩個_InterlockedIncrement(&use_count_)並發履行,這是線程平安的。
2. SP1和SP2都遞加援用計數,即release被並發挪用,也就是_InterlockedDecrement(&use_count_ )並發履行,這也是線程平安的。只不外後履行的線程擔任刪除對象。
3. SP1遞增援用計數,挪用add_ref_copy;SP2遞加援用計數,挪用release。因為SP1的存在,SP2的release操作不管若何都不會招致use_count_變成零,也就是說release中if語句的body永久不會被履行。是以,這類情形就化簡為_InterlockedIncrement(&use_count_)和_InterlockedDecrement( &use_count_ )的並發履行,依然是線程平安的。
然後斟酌weak_ptr。假如是weak_ptr之間的操作,或許從shared_ptr結構weak_ptr,都不觸及到use_count_的操作,只須要挪用weak_add_ref和weak_release來操作weak_count_。與下面的剖析雷同,_InterlockedIncrement和_InterlockedDecrement包管了weak_add_ref和weak_release並發操作的線程平安性。但假如存在從weak_ptr結構shared_ptr的操作,則須要斟酌在結構weak_ptr的進程中,被治理的對象曾經被其他線程被釋放的情形。假如從weak_ptr結構shared_ptr依然是經由過程add_ref_copy函數完成的,則能夠產生以下毛病情形:
線程1,從weak_ptr創立shared_ptr
線程2,釋放今朝獨一存在的shared_ptr
1
斷定use_count_年夜於0,期待履行add_ref_copy
2
挪用release,use_count--。發明use_count為0,刪除被治理的對象
3
開端履行add_ref_copy,招致 use_count遞增。
產生毛病,use_count==1,然則對象曾經被刪除
線程1,從weak_ptr創立shared_ptr
線程2,釋放今朝獨一存在的shared_ptr
線程3,從weak_ptr創立shared_ptr
1
斷定use_count_年夜於0,期待履行add_ref_copy
2
斷定use_count_年夜於0,期待履行add_ref_copy
3
挪用release,use_count--。發明use_count為0,刪除被治理的對象
4
開端履行add_ref_copy,招致 use_count遞增。
5
履行add_ref_copy,招致 use_count遞增。
6
發明use_count_ != 1,斷定履行勝利。
產生毛病,use_count==2,然則對象曾經被刪除
發明use_count_ != 1,斷定履行勝利。
產生毛病,use_count==2,然則對象曾經被刪除
現實上,boost從weak_ptr結構shared_ptr不是挪用add_ref_copy,而是挪用add_ref_lock函數。add_ref_lock是典范的無鎖修正同享變量的代碼,上面再把它的代碼復制一遍,並添加證實正文。
bool add_ref_lock(){
for( ;; )
{
// 第一步,記載下use_count_
long tmp = static_cast< long const volatile& >( use_count_ );
// 第二步,假如曾經被其余線程爭先清0了,則被治理的對象曾經或許將要被釋放,前往false
if( tmp == 0 ) return false;
// 第三步,假如if前提履行勝利,
// 解釋在修正use_count_之前,use_count依然是tmp,年夜於0
// 也就是說use_count_在第一步和第三步之間,歷來沒有變成0過。
// 這是由於use_count一旦變成0,就弗成能再次累加為年夜於0
// 是以,第一步和第三步之間,被治理的對象弗成能被釋放,前往true。
if( _InterlockedCompareExchange( &use_count_, tmp + 1, tmp ) == tmp )return true;
}
}
在下面的正文中,用到了一個沒有被證實的結論,“use_count一旦變成0,就弗成能再次累加為年夜於0”。上面四條可以證實它。
1.use_count_是sp_counted_base類的private對象,sp_counted_base也沒有友元函數,是以use_count_不會被對象外的代碼修正。
2.成員函數add_ref_copy可以遞增use_count_,然則一切對add_ref_copy函數的挪用都是經由過程一個shared_ptr對象履行的。既然存在shared_ptr對象,use_count在遞增之前必定不是0。
3.成員函數add_ref_lock可以遞增use_count_,但正如add_ref_lock代碼所示,履行第三步的時刻,tmp都是年夜於0的,是以add_ref_lock不會使use_count_從0遞增到1
4.其它成員函數歷來不會遞增use_count_
至此,我們可以放下心來,只需add_ref_lock前往true,遞增援用計數的行動就是勝利的。是以從weak_ptr結構shared_ptr的行動也是完整肯定的,要末add_ref_lock前往true,結構勝利,要末add_ref_lock前往false,結構掉敗。
綜上所述,多線程經由過程分歧的shared_ptr或許weak_ptr對象並發修正統一個援用計數對象sp_counted_base是線程平安的。而sp_counted_base對象是這些智能指針獨一操作的同享內存區,是以終究的成果就是線程平安的。
其它操作
後面我們剖析了,分歧的shared_ptr對象可以被多線程同時修正。那其它的成績呢,統一個shared_ptr對象可以對多線程同時修正嗎?我們必需要留意到,後面一切的同步都是針對援用計數類sp_counted_base停止的,shared_ptr自己並沒有任何同步掩護。我們看上面boost文檔舉出來的非線程平安的例子
// thread A
p3.reset(new int(1));
// thread B
p3.reset(new int(2)); // undefined, multiple writes
上面是shared_ptr類相干的代碼
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履行了兩個修正成員變量的操作,thread A和thread B的履行成果能夠長短法的。。
然則模仿內置對象的語義,boost供給了若干個原子函數,支撐經由過程這些函數並發修正統一個shared_ptr對象。這包含atomic_store、atomic_exchange、atomic_compare_exchange等。以下是完成的代碼,不再具體剖析。
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;
}
}
總結
正如boost文檔所傳播鼓吹的,boost為shared_ptr供給了與內置類型同級其余線程平安性。這包含:
1. 統一個shared_ptr對象可以被多線程同時讀取。
2. 分歧的shared_ptr對象可以被多線程同時修正。
3. 統一個shared_ptr對象不克不及被多線程直接修正,但可以經由過程原子函數完成。
假如把下面的表述中的"shared_ptr"調換為“內置類型”也完整成立。
最初,整頓這個器械的時刻我也發明有些症結點很難表述清晰,這也是因為線程平安性自己比擬難嚴厲證實。假如想要完整懂得,照樣建議浏覽shared_ptr完全的代碼。