shared_ptr線程安全性全面分析

正如boost文檔所宣稱的,boost爲shared_ptr提供了與內置類型同級別的線程安全性。這包括:1. 同一個shared_ptr對象可以被多線程同時讀取。2. 不同的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完整的代碼。

您可能感興趣的文章:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章