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在第三行結束後,再判斷一次use_count是否為1,如果是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完整的程式碼。shared_ptr在windows下的原始碼我已經單獨從boost中提取了出來,整理成了單獨的檔案,且去掉了不相關的條件編譯指令。如果有需要的請郵件vigourjiang@gmail.com。如果上面的分析有任何錯誤,也請指出。
原文連結:http://blog.csdn.net/jiangfuqiang/article/details/8292906
相關文章
- 深入理解std::shared_ptr:原理、用法及其執行緒安全性執行緒
- 執行緒安全性執行緒
- 二、執行緒安全性執行緒
- 02. 執行緒安全性執行緒
- 多執行緒的安全性問題(三)執行緒
- Java中primitive type的執行緒安全性JavaMIT執行緒
- 執行緒安全性(第二章)執行緒
- 多執行緒安全性和Java中的鎖執行緒Java
- shared_ptr實現多執行緒讀寫copy-on-write執行緒
- JAVA執行緒dump的分析Java執行緒
- JAVA 併發之路 (二) 執行緒安全性Java執行緒
- 聊一聊Spring中的執行緒安全性Spring執行緒
- Java 執行緒池執行原理分析Java執行緒
- 為什麼多執行緒讀寫shared_ptr需要加鎖執行緒
- webrtc執行緒模型分析Web執行緒模型
- RxJava 執行緒模型分析RxJava執行緒模型
- strerror執行緒安全分析Error執行緒
- 深入解讀HashMap執行緒安全性問題HashMap執行緒
- JCIP閱讀筆記之執行緒安全性筆記執行緒
- 執行緒安全性保證---JMM特性詳解執行緒
- 併發程式設計之執行緒安全性程式設計執行緒
- 探究Spring中Bean的執行緒安全性問題SpringBean執行緒
- Java多執行緒-程式執行堆疊分析Java執行緒
- 關於redis單執行緒的分析Redis執行緒
- java執行緒的狀態+鎖分析Java執行緒
- quartz執行緒管理的原始碼分析quartz執行緒原始碼
- 執行緒池原始碼分析執行緒原始碼
- 多執行緒:原理分析整理執行緒
- 多執行緒分析圖集執行緒
- 一起分析執行緒的狀態及執行緒通訊機制執行緒
- 執行緒、開啟執行緒的兩種方式、執行緒下的Join方法、守護執行緒執行緒
- 多執行緒------執行緒與程式/執行緒排程/建立執行緒執行緒
- 執行緒的建立及執行緒池執行緒
- 深度分析 Java 的列舉型別:列舉的執行緒安全性及序列化問題Java型別執行緒
- 執行緒池之ThreadPoolExecutor執行緒池原始碼分析筆記執行緒thread原始碼筆記
- 執行緒池之ScheduledThreadPoolExecutor執行緒池原始碼分析筆記執行緒thread原始碼筆記
- 多執行緒-執行緒控制之休眠執行緒執行緒
- 多執行緒-執行緒控制之加入執行緒執行緒