shared_ptr的设计哲学(原理+源码):内存安全和性能的架构权衡

0.简介

在C++编程世界中,内存管理是一把双刃剑,手动管理带来了极致的内存控制能力,但也带来了像内存泄漏,野指针等问题;自动垃圾回收虽然安全,但却会带来一定的性能损耗。本文将介绍C++11引入shared_ptr,通过对其原理和源码的解析,来了解其对于内存安全和性能的权衡。

1.原理

要了解一个设计,首先要看这个设计要解决的问题,shared_ptr的核心目标是实现安全的动态内存管理,所以内存安全就是其首要的任务;但也不能只考虑安全,也需要对像性能,易用性进行一些考虑,下面就在这几个方面对shared_ptr的设计原理进行分析。

1.1 安全性设计

1)动态管理:分配控制的RAII实现,通过重载各种操作符来实现引用计数的增减和内存释放。

2)线程安全:通过原子变量以及内存顺序来保证线程安全。

3)释放安全:可以自定义释放使用的Deleter来进行释放,可以利用这个特性做退出作用域的释放,下面源码分析会介绍。

1.2 性能设计

要看性能的设计首先要明确可能导致性能问题的点,第一个就是并发场景下引用计数的共享;然后就是shared_ptr本身控制块的内存分配。

1)对于并发场景下的引用计数,可以看源码解析中的三种枚举,通过默认的原子操作,尽可能的降低影响。

2)对于内存分配,通过提供make_shared来实现一次性的分配,不多次申请内存。

1.3 易用性设计

易用性可以从以下方面进行考虑:

1)支持的类型:通过模板来支持各种类型。

2)创建方式:通过提供make_shared来支持更高效的创建方式。

3)使用方式:通过提供与裸指针一致的使用方式来降低使用要求。

4)和外部的集成:提供和标准库良好的集成。

2.源码解析

源码分析我们先看其数据成员,然后看其主要的函数以及基于其函数我们可以得到的用法。

2.1 数据成员

shared_ptr继承自__shared_ptr,其主要的成员也在__shared_ptr,其成员如下:

element_type*    _M_ptr;         // Contained pointer.
__shared_count<_Lp>  _M_refcount;    // Reference counter.

可以看到其中一个是传入的指针,另外一个是引用计数。传入的指针含义和实现比较明确,我们来看引用计数部分,首先引用计数包含两个部分,一个是__shared_count的模板类,一个是_LP,我们一个个来看,先来看__shared_count,其是引用计数的核心类,其内成员主要是:

_Sp_counted_base<_Lp>*  _M_pi;

通过_M_pi的下面两个函数实现引用计数的加减,其内部封装原子的操作:

void
      _M_weak_add_ref() noexcept
      { __gnu_cxx::__atomic_add_dispatch(&_M_weak_count, 1); }
      void
      _M_weak_release() noexcept
      {
        // Be race-detector-friendly. For more info see bits/c++config.
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
  if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
    {
            _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
      if (_Mutex_base<_Lp>::_S_need_barriers)
        {
          // See _M_release(),
          // destroy() must observe results of dispose()
    __atomic_thread_fence (__ATOMIC_ACQ_REL);
        }
      _M_destroy();
    }
      }

接下来来看_LP,其类型如下:

// Available locking policies:
  // _S_single    single-threaded code that doesn't need to be locked.
  // _S_mutex     multi-threaded code that requires additional support
  //              from gthr.h or abstraction layers in concurrence.h.
  // _S_atomic    multi-threaded code using atomic operations.
   enum _Lock_policy { _S_single, _S_mutex, _S_atomic }; 

默认策略如下:

static const _Lock_policy __default_lock_policy = 
#ifndef __GTHREADS
  _S_single;
#elif defined _GLIBCXX_HAVE_ATOMIC_LOCK_POLICY
  _S_atomic;
#else
  _S_mutex;
#endif

其决定是否需要内存屏障,通过特化模板实现:

template<_Lock_policy _Lp>
    class _Mutex_base
    {
    protected:
      // The atomic policy uses fully-fenced builtins, single doesn't care.
      enum { _S_need_barriers = 0 };
    };
  template<>
    class _Mutex_base<_S_mutex>
    : public __gnu_cxx::__mutex
    {
    protected:
      // This policy is used when atomic builtins are not available.
      // The replacement atomic operations might not have the necessary
      // memory barriers.
      enum { _S_need_barriers = 1 };
    };

2.2 关键函数和利用方式

关键的函数我们来看一些操作符重载以及自定义删除和内存分配器的函数。

1)一些赋值的重载,让其可控。

      __shared_ptr&
      operator=(__shared_ptr&& __r) noexcept
      {
  __shared_ptr(std::move(__r)).swap(*this);
  return *this;
      }
      template<class _Yp>
  _Assignable<_Yp>
  operator=(__shared_ptr<_Yp, _Lp>&& __r) noexcept
  {
    __shared_ptr(std::move(__r)).swap(*this);
    return *this;
  }
      template<typename _Yp, typename _Del>
  _UniqAssignable<_Yp, _Del>
  operator=(unique_ptr<_Yp, _Del>&& __r)
  {
    __shared_ptr(std::move(__r)).swap(*this);
    return *this;
  }
      void
      reset() noexcept
      { __shared_ptr().swap(*this); }

2)自定义内存分配器和释放操作,其中自定义内存分配器直接按照接口定义就可以,较为常用的是自定义释放,可以用来做退出作用域的操作。

template<typename _Yp, typename _Deleter,
         typename = _Constructible<_Yp*, _Deleter>>
  shared_ptr(_Yp* __p, _Deleter __d)
        : __shared_ptr<_Tp>(__p, std::move(__d)) { }
      /**
       *  @brief  Construct a %shared_ptr that owns a null pointer
       *          and the deleter @a __d.
       *  @param  __p  A null pointer constant.
       *  @param  __d  A deleter.
       *  @post   use_count() == 1 && get() == __p
       *  @throw  std::bad_alloc, in which case @a __d(__p) is called.
       *
       *  Requirements: _Deleter's copy constructor and destructor must
       *  not throw
       *
       *  The last owner will call __d(__p)
       */
      template<typename _Deleter>
  shared_ptr(nullptr_t __p, _Deleter __d)
        : __shared_ptr<_Tp>(__p, std::move(__d)) { }
      /**
       *  @brief  Construct a %shared_ptr that owns the pointer @a __p
       *          and the deleter @a __d.
       *  @param  __p  A pointer.
       *  @param  __d  A deleter.
       *  @param  __a  An allocator.
       *  @post   use_count() == 1 && get() == __p
       *  @throw  std::bad_alloc, in which case @a __d(__p) is called.
       *
       *  Requirements: _Deleter's copy constructor and destructor must
       *  not throw _Alloc's copy constructor and destructor must not
       *  throw.
       *
       *  __shared_ptr will release __p by calling __d(__p)
       */
      template<typename _Yp, typename _Deleter, typename _Alloc,
         typename = _Constructible<_Yp*, _Deleter, _Alloc>>
  shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
  : __shared_ptr<_Tp>(__p, std::move(__d), std::move(__a)) { }
      /**
       *  @brief  Construct a %shared_ptr that owns a null pointer
       *          and the deleter @a __d.
       *  @param  __p  A null pointer constant.
       *  @param  __d  A deleter.
       *  @param  __a  An allocator.
       *  @post   use_count() == 1 && get() == __p
       *  @throw  std::bad_alloc, in which case @a __d(__p) is called.
       *
       *  Requirements: _Deleter's copy constructor and destructor must
       *  not throw _Alloc's copy constructor and destructor must not
       *  throw.
       *
       *  The last owner will call __d(__p)
       */
      template<typename _Deleter, typename _Alloc>
  shared_ptr(nullptr_t __p, _Deleter __d, _Alloc __a)
  : __shared_ptr<_Tp>(__p, std::move(__d), std::move(__a)) { }

自定义释放操作可以按照如下方式使用:

void test()
{
    int *pData = new(std::nothrow) int(10);
    std::shared_ptr scope_exit(nullptr,[&](void*){
        if(nullptr != pData)
             delete pData;
        });
}

2.3 问题和处理

shared_ptr会存在循环引用问题,这个可以使用weak_ptr解决,后面会专门对weak_ptr的实现原理和源码进行分析。

3.总结

对于shared_ptr的使用,我们要知道它带来的便利和问题。在开发领域没有银弹,只有取舍,也就是优秀的架构不是选择完美的工具,而是理解每种工具的代价。所以在一些高性能关键领域可以不去使用shared_ptr,而一些常规领域建议使用。

原文链接:,转发请注明来源!