zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    MySQL 8.0.14版本新功能详解
    Uncaught TypeError: Right-hand side of 'instanceof' is not an object
    程序员快速技术提升之道
    Windows 10 cmd命令符的使用
    数据千万条,备份第一条:VFEmail被擦除所有数据面临关停
    netty-socketio 示例代码
    idea中 在接口中如何直接跳转到该接口的是实现类中?
    perl DBD处理超时问题
    springboot 启动配置文件配置
    Office Word 发布文章到博客园
  • 原文地址:https://www.cnblogs.com/gadfly/p/1836992.html
Copyright © 2011-2022 走看看