shared_ptr智能指針源碼剖析

前幾天有個人問了我一個問題: 如何將一個智能指針作爲函數的返回值傳遞出來。當時這個問題一下子把我問倒了,後來經人提醒有一個叫shared_ptr的智能指針可以解決這個問題。

將shared_ptr作爲函數返回值的代碼是這樣的:

#include <tr1/memory>
#include <stdio.h>

using std::tr1::shared_ptr;


shared_ptr<int> ReturnSharedPtr()
{
    shared_ptr<int> p(new int(1000));
    return p;
}

int main()
{
    shared_ptr<int> p1 = ReturnSharedPtr();
    printf("%d\n", *p1);
    return 0;
}

在g++4.3版本以上編譯通過。shared_ptr頭文件的位置有點古怪,在我的DEBIAN(squeeze)機器上的這個地方:/usr/include/c++/4.4/tr1/shared_ptr.h,所以使用時要

#include <tr1/memory>

而且shared_ptr被包裝在std::tr1這個名字空間內。所以使用的時候和一般的stl模板的方式不大一樣, 要using std::tr1這個namespace。也可以用一個編譯器選項去掉這種古怪的定義方法, 具體是那個選項我查不到了。請了解的人幫忙指正一下。

shared_ptr的實現
看了一下stl的源碼,shared_ptr的實現是這樣的: shared_ptr模板類有一個__shared_count類型的成員_M_refcount來處理引用計數的問題。__shared_count也是一個模板類,它的內部有一個指向Sp_counted_base_impl類型的指針_M_pi。所有引用同一個對象的shared_ptr都共用一個_M_pi指針。

當一個shared_ptr拷貝複製時, _M_pi指針調用_M_add_ref_copy()函數將引用計數+1。 當shared_ptr析構時,_M_pi指針調用_M_release()函數將引用計數-1。 _M_release()函數中會判斷引用計數是否爲0. 如果引用計數爲0, 則將shared_ptr引用的對象內存釋放掉。

__shared_count(const __shared_count& __r) 
      : _M_pi(__r._M_pi) // nothrow
      {    
    if (_M_pi != 0)
      _M_pi->_M_add_ref_copy();
      COSTA_DEBUG_REFCOUNT;
      }

這是__shared_count拷貝複製時的代碼。首先將參數__r的_M_pi指針賦值給自己, 然後判斷指針是否爲NULL, 如果不爲null 則增加引用計數。COSTA_DEBUG_REFCOUNT和COSTA_DEBUG_SHAREDPTR是我爲了打印引用計數的調試代碼,會打印文件行號和當前引用計數的值。

#define COSTA_DEBUG_REFCOUNT fprintf(stdout,"%s:%d costaxu debug refcount: %d\n", __FILE__,__LINE__,_M_pi->_M_get_use_count());


#define COSTA_DEBUG_SHAREDPTR fprintf(stdout,"%s:%d costaxu debug \n", __FILE__,__LINE__);
__shared_count&
      operator=(const __shared_count& __r) // nothrow
      {    
    _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
    if (__tmp != _M_pi)
      {    
        if (__tmp != 0)
          __tmp->_M_add_ref_copy();
        if (_M_pi != 0)
          _M_pi->_M_release();
        _M_pi = __tmp;
      }
      COSTA_DEBUG_REFCOUNT;
    return *this;
      }

這是__share_count重載賦值操作符的代碼。 首先,判斷等號左右兩邊的__share_count是否引用同一個對象。如果引用同一個對象(__tmp==_M_pi),那麼引用計數不變,什麼都不用做。如果不是的話,就把等號左邊的share_ptr的引用計數-1,將等號右邊的引用計數+1 。例如: 有兩個shared_ptr p1和p2, 運行p1= p2 。 假如p1和p2是引用同一個對象的,那麼引用計數不變。 如果p1和p2是指向不同對象的,那麼p1所指向對象的引用計數-1, p2指向對象的引用計數+1。

~__shared_count() // nothrow
      {
    if (_M_pi != 0)
      _M_pi->_M_release();

      COSTA_DEBUG_REFCOUNT;
      }

上面是__share_count的析構函數, 其實析構函數只是調用了_M_pi的_M_release這個成員函數。_M_release這個函數,除了會將引用計數-1之外,還會判斷是否引用計數爲0, 如果爲0就調用_M_dispose()函數。 _M_dispose函數會將share_ptr引用的對象釋放內存。

virtual void
      _M_dispose() // nothrow
      {
          COSTA_DEBUG_SHAREDPTR;
          _M_del(_M_ptr);
      }

_M_del是在構造_M_pi時候就初始化好的內存回收函數, _M_ptr就是shared_ptr引用的對象指針。

下面是我寫的一段簡單的測試代碼:

#include <stdio.h>
#include <tr1/memory>

using std::tr1::shared_ptr;


shared_ptr<int> ReturnSharedPtr()
{
    shared_ptr<int> p(new int(1000));
    shared_ptr<int> p2(p);
    shared_ptr<int> p3=p;
    shared_ptr<int> p4; 
    p4=p2;
    return p;
}

int main()
{
    shared_ptr<int> p1 = ReturnSharedPtr();
    printf("%d\n", *p1);
    return 0;
}

下面是運行結果:

shared_ptr.h 169行是__shared_count拷貝構造時增加引用計數,184行是__shared_count賦值操作,161行是__share_count的析構時減少引用計數, 79行是釋放引用對象的內存。

/usr/include/c++/4.4/tr1/shared_ptr.h:169 costaxu debug refcount: 2
/usr/include/c++/4.4/tr1/shared_ptr.h:169 costaxu debug refcount: 3
/usr/include/c++/4.4/tr1/shared_ptr.h:184 costaxu debug refcount: 4
/usr/include/c++/4.4/tr1/shared_ptr.h:161 costaxu debug refcount: 3
/usr/include/c++/4.4/tr1/shared_ptr.h:161 costaxu debug refcount: 2
/usr/include/c++/4.4/tr1/shared_ptr.h:161 costaxu debug refcount: 1
1000
/usr/include/c++/4.4/tr1/shared_ptr.h:79 costaxu debug
/usr/include/c++/4.4/tr1/shared_ptr.h:161 costaxu debug refcount: 0

shared_ptr線程安全性問題

關於shared_ptr的線程安全性。查了一些網上的資料,有的說是安全的,有的說不安全。引用CSDN上一篇比較老的帖子, 它是這樣說的:

“Boost 文檔對於 shared_ptr 的線程安全有一段專門的記述,內容如下:
shared_ptr objects offer the same level of thread safety as built-in types. A shared_ptr instance can be “read” (accessed using only const operations) simultaneously by multiple threads. Different shared_ptr instances can be “written to” (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)
Any other simultaneous accesses result in undefined behavior.
翻譯爲中文如下:
shared_ptr 對象提供與內建類型一樣的線程安全級別。一個 shared_ptr 實例可以同時被多個線程“讀”(僅使用不變操作進行訪問)。 不同的 shared_ptr 實例可以同時被多個線程“寫入”(使用類似 operator= 或 reset 這樣的可變操作進行訪問)(即使這些實 例是拷貝,而且共享下層的引用計數)。
任何其它的同時訪問的結果會導致未定義行爲。”

這幾句話比較繁瑣,我總結一下它的意思:

1 同一個shared_ptr被多個線程“讀”是安全的。

2 同一個shared_ptr被多個線程“寫”是不安全的。

3 共享引用計數的不同的shared_ptr被多個線程”寫“ 是安全的。

如何印證上面的觀點呢?

其實第一點我覺得比較多餘。因爲在多個線程中讀同一個對象,在正常情況下不會有什麼問題。

所以問題就是:如何寫程序證明同一個shared_ptr被多個線程”寫”是不安全的?

我的思路是,在多個線程中同時對一個shared_ptr循環執行兩遍swap。 shared_ptr的swap函數的作用就是和另外一個shared_ptr交換引用對象和引用計數,是寫操作。執行兩遍swap之後, shared_ptr引用的對象的值應該不變。

程序如下:

#include <stdio.h>
#include <tr1/memory>
#include <pthread.h>

using std::tr1::shared_ptr;

shared_ptr<int> gp(new int(2000));

shared_ptr<int>  CostaSwapSharedPtr1(shared_ptr<int> & p)
{
    shared_ptr<int> p1(p);
    shared_ptr<int> p2(new int(1000));
    p1.swap(p2);
    p2.swap(p1);
    return p1;
}

shared_ptr<int>  CostaSwapSharedPtr2(shared_ptr<int> & p)
{
    shared_ptr<int> p2(new int(1000));
    p.swap(p2);
    p2.swap(p);
    return p;
}


void* thread_start(void * arg)
{
    int i =0;
    for(;i<100000;i++)
    {
        shared_ptr<int> p= CostaSwapSharedPtr2(gp);
        if(*p!=2000)
        {
            printf("Thread error. *gp=%d \n", *gp);
            break;
        }
    }
    printf("Thread quit \n");
    return 0;
}



int main()
{
    pthread_t thread;
    int thread_num = 10, i=0;
    pthread_t* threads = new pthread_t[thread_num];
    for(;i<thread_num;i++)
        pthread_create(&threads[i], 0 , thread_start , &i);
    for(i=0;i<thread_num;i++)
        pthread_join(threads[i],0);
    delete[] threads;
    return 0;
}

這個程序中我啓了10個線程。每個線程調用10萬次 CostaSwapSharedPtr2函數。 在CostaSwapSharePtr2函數中,對同一個share_ptr全局變量gp進行兩次swap(寫操作), 在函數返回之後檢查gp的值是否被修改。如果gp值被修改,則證明多線程對同一個share_ptr執行寫操作是不安全的。

程序運行的結果如下:

Thread error. *gp=1000
Thread error. *gp=1000
Thread quit
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread error. *gp=1000
Thread quit
Thread quit
10個線程有9個出錯。證明多線程對同一個share_ptr執行寫操作是不安全的。我們在程序中,如果不運行CostaSwapSharedPtr2, 改成運行CostaSwapSharedPtr1呢? CostaSwapSharedPtr1和CostaSwapSharedPtr2的區別在於, 它不是直接對全局變量gp進行寫操作,而是將gp拷貝出來一份再進行寫操作。運行的結果如下:
costa@pepsi:~/test/cpp/shared_ptr$ ./b
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
Thread quit
跑了很多次都沒有出錯。說明共享引用計數的不同的shared_ptr執行swap是線程安全的。BOOST文檔是可信的。

補充一個問題: 爲什麼shared_ptr可以作爲STL標準容器的元素,而auto_ptr不可以

這篇文章小結一下:

1 shared_ptr是一個非常實用的智能指針。

2 shared_ptr的實現機制是在拷貝構造時使用同一份引用計數。

3 對同一個shared_ptr的寫操作不是線程安全的。 對使用同一份引用計數的不同shared_ptr是線程安全的。

轉載自:https://my.oschina.net/costaxu/blog/103119

發佈了398 篇原創文章 · 獲贊 71 · 訪問量 63萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章