boost::shared_ptr 分析与实现

http://blog.chinaunix.net/u/14337/showart_299314.html

/************************************************************
* file: shared_ptr
*
* desc: 本文将对boost::shared_ptr作一详细的介绍。 本文介绍的不是用法,而是
* 智能指针的原理,结构以及boost对其的实现. 最后还会给出一个简化了的实现。
*
* author: whiteear
* date: 2007-05-10
* copyright: 任意发布
***********************************************************/


shared_ptr

1 boost 介绍

关于boost的介绍, 网上一大堆。 google一下就行。 作为初学者, 也没有能力去全面介绍它。

2 shared_ptr 介绍

在boost库中, 智能指针并不只shared_ptr一个。同族但有不同的功能目标的还有如下5个:
scoped_ptr
scoped_array
shared_ptr
shared_array
weak_ptr

前面两个, 与标准C++中的智能指针auto_ptr功能基本类似, 不过它不传递所有权, 不可复制。
从其名称就可以看出, 其主要目标就是在小范围, 小作用域中使用,以减少显式的delete, new配对操作,
提高代码的安全性。scoped_ptr是针对指针的版本, 而scoped_array同是专门针对数组的。(记住, 删除
一个指针使用delete p; 而删除一个数组使用delete[] p);

后面两个是本文的重点,它们在所有对象中, 共享所指向实体的所有权。 即只要是指向同一个实体
对象的shared_ptr对象, 都有权操作这个对象,并根据自己产生新的对象,并把所有权共享给新的对象。即它
是满足STL对对象的基本要求可复制,可赋值的。可以与所有的STL容器,算法结合使用。顾名思义, shared_ptr
是针对任意类型的指针的, 而shared_array则是专门针对任意类型的数组的。

最后一个weak_ptr。

3 shared_ptr 分析

3.1 一些思考

C++对内存数据的直接可操作性以及指针的使用,给编程带来了极大的灵活,
但同时也还来了很大的问题。 程序在运行时,如果出现segment fault,
大部分原因在于非法访问了内存。
或者访问了一个越界的地址,或者访问了一个已经失效的指针。我们可以随时、随地的NEW一个对象, 并把它赋值给某个指针。
在以后随时要吧通过此指针访问这个对象。
但问题时NEW出来的对象在内存的堆空间中,并不是函数调用栈中,也就是说一个New出来的对象,从生命期上来说,
是全局的, 只要不Delete, 它就一直存在。 相对的实际的内存是有限的,
我们不能只索取而不释放, 否则终有一刻会内存耗尽,New不出新对象。

问题1: 什么时候释放?
New很简单, Delete也很简单。 一个New配对一个Delete也很简单。但不简单的就是什么时候New, 什么时候Delete。New还好办些,
有需求的时候就可以New, 那什么时候一个New出来的对象不再有用呢?什么时候它该被Delete呢?
对于这个问题, Java采用的时GC的机制,由虚拟机通过一定的算法实现。
但Java的GC机制被人诟病也不是一天两天了。 它太慢,太吃内存了。
C++因为各种各样的原因而没有采用GC, 把这个问题直接丢给了程序员。

问题2: 资源申请即初始化
在C++之父Bjarne Stroustrup老大的巨著《The C++ Programming Language》第14章,
第4节资源管理介绍了“资源申请即初始化”。 利用局部对象管理资源的技术通常被说成是“资源申请即初始化”。
这种技术依赖于构造函数和析构函数的特性, 以及它们与异常机制的关系。

我们知道, 一个对象在超出它的作用域时, 会自动被析构。 那么我们就可以设想, 可不可以将New
出来的对象,托管给另一个对象,让它自动的在超出作用域时, 去释放New出来的对象。 这样我们就可以
高枕无忧的去New, 释放的时机就转嫁给托管对象(事实上转嫁给了编译器,因为它知道什么时候这个对
象超出作用域)。

问题3: 托管的资源
当一个New出来的资源被托管之后, 我们所有针对此资源的操作就应该通过这个代理商来进行。当然我
们也可以绕过这个代理商直接与资源对话。这时候我们是不是又回归到了原始的那种状态, 在得到一部分
灵活性, 直接性的同时, 也失去了代理商所具有的管理功能。

事实上, 考查一下C++的指针就会知道, 指针本身也是一个代理商。 只不过它是效率最高的代理商(也许
是资源厂商自己设置的一级代码吧,)。 通过指针我们可以间接的访问它所指代的实体对象。 通过*(dereference)
操作可以得到实体, 通过->可以直接访问实体的内容。 抽象这些特征(类似于STL中Iterator的概念),
我们的新的代理也应该具有这样的功能。

问题4: 释放时机的确定
通过回答上面3个问题。我们似乎已经解决了所有的问题。
我们得到了一个代理, 将我们的资源托管给它。通过代理我们以熟悉的方式访问,
操作资源。在合适的时候代理自动帮助我们释放掉不再使用的资源。 一切都很美好!!!

类似下面的代码应该可以很好的工作:
work()
{
resource r = new resource();
proxy p(r);
// use p to do something
}

当work被执行完成时, 资源r自动被p释放掉。 我们得到的益处好象并不多,
只是少写了几个delete语句。
考虑一下,r是在堆栈中的,所以它的生命期是比较长的,
我们如果想在work之外使用r呢? 如果同时有多个p的复本时呢?
在那一个p被析构时真正的释放这个唯一的资源呢?

考虑一下现实生活中, 一个资源厂商通常会有多家代理,
任意一家代理都可以处理这个资源。 考虑一下,
如果没有任何一个代理愿意去代理这个资源厂商的资源,是不是表明这个资源并不被市场所接收,即它是无用的,应该被淘汰的。
这个时候厂商就应该去相关部门申请耍破产了。

我们的代理也应该这样,它自动的统计有多少个对当前资源的代理,
当没有代理去托管这个资源时, 就应该释放这个资源(delete)。

问题5: 同步
现在多线程编程已经非常普遍, 大家都喜欢一心二用, 三用, 甚至N用。
如果采用上面4个问题的回答来解决资源管理的问题。那么对第4个问题的回答就还缺少点。

当我们使用一个记数器去记录当前对此资源的代理有多少个时,
这个记数器本身是不是也成为一个关键性的资源!!!
对记数器的操作特别是有新代理加入或某个代理析构时, 都要对记数进行修改。
随着多线程的加入,就必须要保证记数器值在所有的代理中都是一样的, 是同步的。 否则后果真是难以想象。

因此在我们的代理设计中, 必须要对记数器部分作线程同步。


3.2 boost::shared_ptr的解决方案


上面我们分析了shared_ptr问题的产生,以及可能的实现方式和应该注意的问题。 下面我们来看看boost::shared_ptr是如何实现上面的思想的。

3.2.1 文件结构
定义boost::shared_ptr主要涉及到以下文件

shared_ptr.hpp
  detail/shared_count.hpp
  detail/sp_counted_base.hpp
  detail/sp_counted_base_pt.hpp
  detail/sp_counted_base_win32.hpp
  detail/sp_counted_base_nt.hpp
  detail/sp_counted_base_pt.hpp
  detail/sp_counted_base_gcc_x86.hpp
  ...   
  detail/sp_counted_base_impl.hpp

涉及的类主要有以下几个:
shared_ptr
shared_count
sp_counted_base
sp_counted_base_impl

其中
shared_ptr定义在shared_ptr.hpp中
share_count定义在shared_count.hpp中
sp_counted_base定义在sp_counted_base_XXX.hpp中。
XXX指代针对特定平台的实现。
sp_counted_base_impl定义在sp_counted_base_impl.hpp中

3.2.2 类功能
sp_counted_base 是问题4中, 记数器的实现。
针对不同的平台使用了不同的同步机制。 如pt是针对linux, unix平台使用pthread接口进行同步的。
win32是针对windows平台, 使用InterlockedIncrement, InterlockedDecrement机制。
gcc_x86是针对AMD64硬件平台的, 内部使用汇编指令实现了atomic_increment,
atomic_decrement。

sp_counted_base_impl 是个模板, 它继承自sp_counted_base,
主要实现了父类中一个纯虚函数dispose。具体的由它来负责在记数值到0(即没有代理时)释放所托管的资源。

shared_count是个类模板。它存在的意义在于和代理类shared_ptr同生共死,
在构造函数中生成记数器,在代理的传递过程中驱动记数器增减。
shared_ptr是个类模板。 它是托管资源的代理类, 所有对资源的操作,
访问都通过它来完成。

下图展示了它们之间的相互关系









figure01 shared_ptr.jpeg





3.2.3 类详细介绍

shared_ptr 类模板。 它是我们直接使用的代理类。
两个属性
pn : shared_count 记数器类。 shared_ptr完全操作pn的生命期,
在构造时构造它,在析构时自动析构它。
这些都是利用构造与析构函数的特性自动完成的。

template<class Y>
explicit shared_ptr( Y * p ): px( p ), pn( p ) // Y must be
complete
{
detail::sp_enable_shared_from_this( pn, p, p );
}

px : T* 它是代理的资源的指针。 所有针对资源的操作*,
->都直接使用它来完成。

reference operator* () const // never throws
{
BOOST_ASSERT(px != 0);
return *px;
}

T * operator-> () const // never throws
{
BOOST_ASSERT(px != 0);
return px;
}


两个方法:
operator=完成代理的赋值传递。
通过这样,就可以有多个代理同时托管同一个资源。在众多的代理中, 它们共享所托管资源的操作权。

shared_ptr & operator=(shared_ptr const & r) // never throws
{
px = r.px;
pn = r.pn; // shared_count::op= doesn't throw
return *this;
}

T* get()资源原始位置的获取。通过这个方法,我们可以直接访问到资源,
并可以对它进行操作。

T * get() const // never throws
{
return px;
}

user_count返回当前资源的代理个数,即有多少个对些资源的引用。

long use_count() const // never throws
{
return pn.use_count();
}


为了方便操作,并完全模拟原生指针的行为,boost::shared_ptr还定义了大量的其它操作函数。


shared_count类 记数器的包装。
一个属性:
pi : sp_counted_base *它指向真正的记数器。

构造函数:
template<class Y> explicit shared_count( Y * p ): pi_( 0 )
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
, id_(shared_count_id)
#endif
{
#ifndef BOOST_NO_EXCEPTIONS

try
{
pi_ = new sp_counted_impl_p<Y>( p );
}
catch(...)
{
boost::checked_delete( p );
throw;
}

#else

pi_ = new sp_counted_impl_p<Y>( p );

if( pi_ == 0 )
{
boost::checked_delete( p );
boost::throw_exception( std::bad_alloc() );
}

#endif
}


当使用一个资源指针来构造一个shared_count时,它知道针对此资源要生成一个代理。
所以生成一个记数器pi。如果在构造记数器的过程中出现任何异常行为,即记数器资源的初始化未成功完成时, 就释放掉资源。
(这就是资源申请即初始化, 对于一个资源管理类来说,要不所有资源申请成功, 要不构造失败)

operator=赋值函数。
在shared_ptr被赋值的时候,会调用它。经过复制后一个shared_ptr变成两个, 所以要对记数器进行增加。
同时如果被赋值的代理原有托管的资源将被释放。

shared_count & operator= (shared_count const & r) // nothrow
{
sp_counted_base * tmp = r.pi_;

if( tmp != pi_ )
{
if( tmp != 0 ) tmp->add_ref_copy();
if( pi_ != 0 ) pi_->release();
pi_ = tmp;
}

return *this;
}

析构函数析构函数在shared_ptr超出作用域被析构时自动调用。
每析构一个shared_ptr,则代理数就少一个, 所以调用记数器的release函数,
减少记数值。

~shared_count() // nothrow
{
if( pi_ != 0 ) pi_->release();
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
id_ = 0;
#endif
}

sp_counted_base 记数器类。
这是一个普通类,没有被模板化。它定义了记数器公用的所有操作, 并实现了同步。

两个属性
use_count记录当前的代理数目。
mutex互斥锁(在不同平台的实现中,是不同的类型。以linux为例,
是pthread_mutex_t)


构造函数初始化互斥锁和记数
sp_counted_base(): use_count_( 1 ), weak_count_( 1 )
{
#if defined(__hpux) && defined(_DECTHREADS_)
pthread_mutex_init( &m_, pthread_mutexattr_default );
#else
pthread_mutex_init( &m_, 0 );
#endif
}

析构函数释放互斥锁
virtual ~sp_counted_base() // nothrow
{
pthread_mutex_destroy( &m_ );
}

同步增加记数值,即根据原有的复制构造出或通过赋值产生新的代理时记数加1
void add_ref_copy()
{
pthread_mutex_lock( &m_ );
++use_count_;
pthread_mutex_unlock( &m_ );
}

释放函数,在代理超界被析构时使用。 它首先减少引用记数,
然后查看记数值,如果为0, 则调用dispose释放资源。并销毁掉本身
void release() // nothrow
{
pthread_mutex_lock( &m_ );
long new_use_count = --use_count_;
pthread_mutex_unlock( &m_ );

if( new_use_count == 0 )
{
dispose();
weak_release();
}
}

释放托管资源的函数。 为纯虚函数, 要求在子类中实现。
只所以不同这个类中实现,是由于要托管的类型是未知的,如果要实现则要记录这个资源,则此类不得不模板化。
将此功能分离到子类中的好处就非常明显。子类可以直接利用父类的功能,只要模板化一下,实现一个函数就行。
这样会大大加快编译速度。

virtual void dispose() = 0; // nothrow

销毁函数。 在shared_count中的pi即记数器是New出来的。
所以在适当的时候要销毁掉。 而这个时机就是资源被释放的时候。
即记数器完成了对资源的管理, 同时完成了对自身的管理。

// destroy() is called when weak_count_ drops to zero.
virtual void destroy() // nothrow
{
delete this;
}


sp_counted_base_impl 类模板。
一个属性

px : T*记录各种类型的资源。

实现了父类的dispose函数。 实现对资源的释放。
virtual void dispose() // nothrow
{
#if defined(BOOST_SP_ENABLE_DEBUG_HOOKS)
boost::sp_scalar_destructor_hook( px_, sizeof(X), this );
#endif
boost::checked_delete( px_ );
}

3.2.4 工作流程

1 用户申请一个资源p
p = new Something();

2 用户将资源p托管给shared_ptr;
shared_ptr(p);

2.1 shared_ptr 构造函数调用,构造出记数对象shared_count
shared_ptr() : px(p), pn(p)

2.1.1 shared_count 构造函数调用,构造出户数器对象
pi = new sp_counted_base_impl(p);

3 用户使用shared_ptr对象sp, 就象直接使用资源一样
*sp;
sp->func();
..

4 如果用户要传递sp
shared_ptr<T> spt = sp;

4.1 调用shared_ptr.operator=
4.1.1 调用shared_count.operator=
4.1.1.1 调用sp_counted_base复制构造
4.1.2 调用sp_counted_base.add_ref_copy, 增加引用记数

5 如果sp超出了它本身的作用域, 则调用析构函数
5.1 调用shared_count.~shared_count 析构记数
5.1.1 调用sp_counted_base.release, 减少引用记数
如果引用记数已经减少到0,则
调用sp_counted_base_impl.dispose, 销毁资源。
调用sp_counted_base.destroy, 销毁自身。

4 实践

上面是对boost::shared_ptr的分析。 整个过程就是对smart
pointer这种观点的思考过程。 基本上是想到什么记录什么。 光说不练不行,
根据对boost::shared_ptr的分析, 本人仿作了一个shared_ptr,
可以看成是对boost::shared_ptr的一个简略版。 有没有用先不说, 反正都是写程序。

全部原代码见附件shared_ptr.tar.gz

5 参考
[1] boost www.boost.org
[2] shared_ptr性能分析
http://blog.csdn.net/ralph623/archive/2005/08/18/458414.aspx
[3] smart pointer in boost
http://www.ddj.com/dept/cpp/184401507?pgno=1
[4] the new C++: smart pointer http://www.ddj.com/dept/cpp/184403837
[5] C++中的智能指针
http://www.stlchina.org/twiki/bin/view.pl/Main/BoostProgrammSmartPoint




posted on 2011-03-21 18:12  RocZhang  阅读(591)  评论(0编辑  收藏  举报