程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> shared_ptr線程平安性周全剖析

shared_ptr線程平安性周全剖析

編輯:關於C++

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在第三行停止後,再斷定一次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完全的代碼。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved