前幾天有個人問了我一個問題: 如何將一個智能指針作爲函數的返回值傳遞出來。當時這個問題一下子把我問倒了,後來經人提醒有一個叫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是線程安全的。