shared_ptr智能指针源码剖析

前几天有个人问了我一个问题: 如何将一个智能指针作为函数的返回值传递出来。当时这个问题一下子把我问倒了,后来经人提醒有一个叫shared_ptr的智能指针可以解决这个问题。

将shared_ptr作为函数返回值的代码是这样的:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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,所以使用时要

?
1
#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引用的对象内存释放掉。


?
1
2
3
4
5
6
7
__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是我为了打印引用计数的调试代码,会打印文件行号和当前引用计数的值。


?
1
2
3
4
#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__);



?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__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。




?
1
2
3
4
5
6
7
~__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引用的对象释放内存。




?
1
2
3
4
5
6
virtual void
       _M_dispose() // nothrow
       {
           COSTA_DEBUG_SHAREDPTR;
           _M_del(_M_ptr);
       }
_M_del是在构造_M_pi时候就初始化好的内存回收函数, _M_ptr就是shared_ptr引用的对象指针。


下面是我写的一段简单的测试代码:


?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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行是释放引用对象的内存。



?
1
2
3
4
5
6
7
8
9
/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引用的对象的值应该不变。

程序如下:


?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#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执行写操作是不安全的。

程序运行的结果如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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拷贝出来一份再进行写操作。运行的结果如下:
?
1
2
3
4
5
6
7
8
9
10
11
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是线程安全的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值