智能指針之share_ptr源碼剖析以及線程安全測試


shared_ptr的實現

看了一下stl的源碼,shared_ptr的實現是這樣的:  shared_ptr模板類有一個__shared_count類型的成員,_M_refcount來處理引用計數的問題。__shared_count也是一個模板類,它的內部有一個指針_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)

 

 

{

 

 

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引用的對象指針。

下面是我自己實現的share_ptr

#include<iostream>
#include<vector>
/*
1.auto_ptr 所有權唯一,只能有一個對象使用
1.智能指針提前失效

2.帶標志位的智能指針 所有權不唯一 釋放權唯一

3.boost:: scope_ptr
*/
class Ref_Management
{
public:
static Ref_Management* getInstance()//引用計數類的接口,模擬__shared_count

{
return &rm;
}
private:
Ref_Management():cursize(0){}
Ref_Management(const Ref_Management&);
static Ref_Management rm;
public:
//添加一個指針指向一塊內存
void addref(void* mptr)
{
if (mptr != NULL)
{
int index = find(mptr);
if (index < 0)
{
Node tmp(mptr, 1);
node[cursize++] = tmp;
//node[cursize].addr = mptr;
//node[cursize].ref = 1;
//cursize++;
}
else
{
node[index].ref++;
}

//std::vector<Node>::iterator fit = find(mptr);
//if (fit == vec.end())
//{
// Node node(mptr, 1);
// vec.push_back(node);
//}
//else
//{
// (*fit).ref++;
//}
}
}
//刪除一個指針
void delref(void* mptr)
{
if (mptr != NULL)
{
int index = find(mptr);
if (index < 0)
{
throw std::exception("addr is not exsit!");
}
else
{
if (node[index].ref != 0)
{
node[index].ref--;
}
}
}
}
//查找指針所在的位置的引用個數
int getref(void* mptr)
{
int rt = -1;
if (mptr != NULL)
{
int index = find(mptr);
if (index >= 0)
{
rt = node[index].ref;
}
}
return rt;
}
private:
//查找指針指向的位置
int find(void* mptr)
{
int rt = -1;
for (int i = 0; i < cursize; i++)
{
if (node[i].addr == mptr)
{
rt = i;
break;
}
}
return rt;
/*std::vector<Node>::iterator it = vec.begin();
for (it; it != vec.end(); it++)
{
if ((*it).addr == mptr)
break;
}
return it;*/
}
//addr:存放的指針,ref:存放指針的位置的個數,引用計數
class Node
{
public:
Node(void* padd = NULL, int rf = 0) :addr(padd), ref(rf){}
public:
void* addr;
int ref;
};
Node node[10];//數組的整體大小,數組中存放的是指針和指針所在位置的個數
int cursize;//數組當前的大小
};
Ref_Management Ref_Management::rm;
template<typename T>
class Shared_Ptr
{
public:
//構造函數,調用AddRef();添加內存
Shared_Ptr(T* ptr = NULL) :mptr(ptr)
{
AddRef();
}
//拷貝構造函數 ,申請一個新的內存,引用計數為一,將數組的當前大小加一
Shared_Ptr(const Shared_Ptr<T>& rhs) :mptr(rhs.mptr)
{
AddRef();
}
//賦值運算符的重載函數,自賦值的的判斷,調用~Share_ptr(),資源復制,調用AddRef();
Shared_Ptr<T>& operator=(const Shared_Ptr<T>& rhs)
{
if (this != &rhs)
{
this->~Shared_Ptr();
mptr = rhs.mptr;
AddRef();
}
return *this;
}
//析構函數,調用DelRef(),釋放內存,並指向NULL;
~Shared_Ptr()
{
DelRef();
if (GetRef() == 0)
{
delete mptr;
}
mptr = NULL;
}
//->運算符重載函數
T* operator->()
{
return mptr;
}
// * 運算符重載函數
T& operator*()
{
return *mptr;
}
private:
//調用addref(),傳入mptr,添加內存
void AddRef()
{
prm->addref(mptr);
}
//調用delref(),傳入mptr,釋放內存
void DelRef()
{
prm->delref(mptr);
}
//調用getref(),傳入mptr,得到當前位置的引用計數
int GetRef()
{
return prm->getref(mptr);
}
T* mptr;
static Ref_Management* prm;//提供靜態的接口,供本類可以調用prm對象的函數
};
template<typename T>
Ref_Management* Shared_Ptr<T>::prm = Ref_Management::getInstance();

class B;
class A
{
public:
A()
{
std::cout << "A()" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
public:
Shared_Ptr<B> spa;
};
class B
{
public:
B()
{
std::cout << "B()" << std::endl;
}
~B()
{
std::cout << "~B()" << std::endl;
}
public:
Shared_Ptr<A> spb;
};
/*
shared_ptr 相互引用
*/
/*
weak_ptr解決問題
*/
int main()
{
Shared_Ptr<A> pa(new A());
Shared_Ptr<B> pb(new B());
pa->spa = pb;
pb->spb = pa;
return 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對象提供與內置類型相同的線程安全級別。多個線程可以同時“讀取”(僅使用常量操作訪問)共享_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>

 

 

usingstd::tr1::shared_ptr;

 

 

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

 

 

shared_ptr<int> CostaSwapSharedPtr1(shared_ptr<int> & p)

 

 

{

 

 

shared_ptr<int> p1(p);

 

 

shared_ptr<int> p2(newint(1000));

 

 

p1.swap(p2);

 

 

p2.swap(p1);

 

 

returnp1;

 

 

}

 

 

shared_ptr<int> CostaSwapSharedPtr2(shared_ptr<int> & p)

 

 

{

 

 

shared_ptr<int> p2(newint(1000));

 

 

p.swap(p2);

 

 

p2.swap(p);

 

 

returnp;

 

 

}

 

 

void* thread_start(void* arg)

 

 

{

 

 

inti =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");

 

 

return0;

 

 

}

 

 

int main()

 

 

{

 

 

pthread_tthread;

 

 

intthread_num = 10, i=0;

 

 

pthread_t* threads = newpthread_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;

 

 

return0;

 

 

}

 

 

 

這個程序中我啟了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不可以    這個根據auto_ptr相信也可以找出答案了,以及auto_ptr為什么會被慢慢摒棄了;

這篇文章小結一下:

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

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

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

線程安全測試取自:http://my.oschina.net/costaxu/blog/103119


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM