ToB企服应用市场:ToB评测及商务社交产业平台

标题: C++智能指针学习——小谈引用计数 [打印本页]

作者: 我可以不吃啊    时间: 2024-5-16 15:09
标题: C++智能指针学习——小谈引用计数
目录

媒介

本文结合源码讨论std::shared_ptr和std::weak_ptr的部分底层实现,然后讨论引用计数,弱引用计数的创建和增减。
文章中尽可能的先叙述原理,然后再贴上代码。假如有不想看代码的,直接略过代码即可。
本文涉及的源码均出自gcc 9.4.0版本
控制块简介

控制块是shared_ptr和weak_ptr中的重要组成,主要用于管理资源的引用计数和生命周期。这个机制允许智能指针安全地共享和管理同一个对象,同时自动释放不再需要的资源。
控制块包含以下部分:
本文讨论的引用计数和弱引用计数的创建、加减、烧毁,与控制块密切相干。
共享控制块

起首我们要知道,当创建一个std::shared_ptr指向某个对象时,会天生一个控制块来存储该对象的引用计数和其他管理信息。假如基于这个std::shared_ptr再创建一个或多个std::weak_ptr,那么这些std::weak_ptr将也指向这个控制块。
表示图大概长如许:

引用计数与弱引用计数创建过程

在谈引用计数和弱引用计数的创建时,其实就是讨论控制块的创建。
我们知道std::weak_ptr是被设计用来解决std::shared_ptr智能指针可能导致的循环引用问题。一个有效的std::weak_ptr对象一般是通过std::shared_ptr构造的或者是通过拷贝(移动)其他std::weak_ptr对象得到的,std::weak_ptr对象的构造不涉及控制块的创建。
因此在讨论引用计数、弱引用计数的创建时,我们是去分析std::shared_ptr的源码
__shared_ptr

__shared_ptr是std::shared_ptr的焦点实现,它位于shared_ptr_base.h中。
__shared_ptr在构造实例时都会构造一个_M_refcount,它的类型为__shared_count。
  1. //file: shared_ptr_base.h
  2. template<typename _Tp, _Lock_policy _Lp>
  3. class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
  4. {
  5. public:
  6.         using element_type = typename remove_extent<_Tp>::type;
  7.         //默认构造
  8.         constexpr __shared_ptr() noexcept
  9.       : _M_ptr(0), _M_refcount()
  10.       { }
  11.         ...       
  12.         //有删除器和分配器的构造
  13.         template<typename _Yp, typename _Deleter, typename _Alloc,
  14.                typename = _SafeConv<_Yp>>
  15.         __shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
  16.         : _M_ptr(__p), _M_refcount(__p, std::move(__d), std::move(__a))
  17.         {
  18.           static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
  19.               "deleter expression d(p) is well-formed");
  20.           _M_enable_shared_from_this_with(__p);
  21.         }
  22. private:
  23.         ...
  24.         element_type*        _M_ptr;         // Contained pointer.
  25.     __shared_count<_Lp>  _M_refcount;    // Reference counter.       
  26. };
复制代码
__shared_count

在创建__shared_count对象时,也会创建一个对应的控制块(_Sp_counted_base的派生类的实例),用于管理__shared_ptr所指向的资源。__shared_count通过指向这个控制块的多态指针来管理引用计数和资源。
代码中的_Sp_counted_ptr和_Sp_counted_deleter就是_Sp_counted_base的派生类。
  1. //file: shared_ptr_base.h
  2. template<_Lock_policy _Lp>
  3. class __shared_count
  4. {
  5. public:
  6.         //默认构造
  7.         __shared_count(_Ptr __p) : _M_pi(0)
  8.         {
  9.             __try
  10.             {
  11.                 _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
  12.             }
  13.             __catch(...)
  14.             {
  15.                 delete __p;
  16.                 __throw_exception_again;
  17.             }
  18.         }
  19.         //带分配器和删除器的构造
  20.         template<typename _Ptr, typename _Deleter, typename _Alloc,
  21.                typename = typename __not_alloc_shared_tag<_Deleter>::type>
  22.         __shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
  23.         {
  24.             typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
  25.             __try
  26.             {
  27.                 typename _Sp_cd_type::__allocator_type __a2(__a);
  28.                 auto __guard = std::__allocate_guarded(__a2);
  29.                 _Sp_cd_type* __mem = __guard.get();
  30.                 ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
  31.                 _M_pi = __mem;
  32.                 __guard = nullptr;
  33.             }
  34.             __catch(...)
  35.             {
  36.                 __d(__p); // Call _Deleter on __p.
  37.                 __throw_exception_again;
  38.             }
  39.         }
  40. private:
  41.         friend class __weak_count<_Lp>;
  42.         _Sp_counted_base<_Lp>*  _M_pi;
  43. };
复制代码
_Sp_counted_base

_Sp_counted_base负责管理引用计数和弱引用计数,其中
我们可以看到在_Sp_counted_base的初始化列表中,初始化了_M_use_count和_M_weak_count为1,完成了引用计数和弱引用计数的创建和初始化。
  1. //file: shared_ptr_base.h
  2. template<_Lock_policy _Lp = __default_lock_policy>
  3. class _Sp_counted_base : public _Mutex_base<_Lp>
  4. {
  5. public:
  6.     _Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { }
  7.         ...       
  8. private:
  9.         _Atomic_word  _M_use_count;     // #shared
  10.         _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
  11. };
复制代码
这里再简单提一下_Sp_counted_base、_Sp_counted_ptr和_Sp_counted_deleter的关系与各自的功能。
因为_Sp_counted_base是抽象基类无法被实例化,以是利用的是其派生类_Sp_counted_ptr和_Sp_counted_deleter对象来管理引用计数、弱引用计数、分配器、删除器。这个对象就是我们常说的控制块。
(_Sp_counted_base还有一个派生类_Sp_counted_ptr_inplace,适合利用std::make_shared的场景,此处不外多讨论)
弱引用计数增长过程

再谈共享控制块

在上面的引用计数与弱引用计数创建过程中,我们提到:
一个有效的std::weak_ptr对象一般是通过std::shared_ptr构造的或者是通过拷贝(移动)其他std::weak_ptr对象得到的
对应的__weak_count和__shared_count对象也具有上述关系。
查看源码,我们可以发现,__weak_count和__shared_count都有一个指向控制块的多态指针。
  1.         _Sp_counted_base<_Lp>*  _M_pi;
复制代码
在__weak_count中并没有利用new或者类似操作让_M_pi指向一块新的内存(控制块)。追根溯源,__weak_count中多态指针指向的控制块的泉源就是__shared_count。代码中是通过在__weak_count构造函数和重载的赋值运算符中给多态指针_M_pi初始化和赋值实现的。以此实现了weak_ptr和shared_ptr共享控制块的功能。
__weak_count

弱引用计数的增长可以分为下面几种环境:
其实本质是靠调用_M_weak_add_ref()增长的弱引用计数,详情见__weak_count的源码:
  1. //file: shared_ptr_base.htemplateclass __weak_count{public:        ...        //通过__shared_count构造    //和一个已存在的__shared_count对象共享控制块,并更新控制块的弱引用计数    __weak_count(const __shared_count& __r) noexcept     : _M_pi(__r._M_pi)    {            //若入参的多态指针不为空        //弱引用计数++(增长_Sp_counted_base对象的_M_weak_count)                if (_M_pi != nullptr)                        _M_pi->_M_weak_add_ref();        }                //通过__weak_count拷贝构造    //和传入的__weak_count对象就共享同一个控制块,并更新控制块的弱引用计数    __weak_count(const __weak_count& __r) noexcept     : _M_pi(__r._M_pi)    {                if (_M_pi != nullptr)                        _M_pi->_M_weak_add_ref();    }            //通过__shared_count给__weak_count赋值        __weak_count& operator=(const __shared_count& __r) noexcept    {            _Sp_counted_base* __tmp = __r._M_pi;            //新对象弱引用计数++                if (__tmp != nullptr)                          __tmp->_M_weak_add_ref();                  //原对象弱引用计数--                if (_M_pi != nullptr)                          _M_pi->_M_weak_release();                  //指向新对象的控制块                _M_pi = __tmp;                return *this;        }        //通过__weak_count给__weak_count赋值        __weak_count& operator=(const __weak_count& __r) noexcept    {                _Sp_counted_base* __tmp = __r._M_pi;                if (__tmp != nullptr)                          __tmp->_M_weak_add_ref();                if (_M_pi != nullptr)                          _M_pi->_M_weak_release();                _M_pi = __tmp;                return *this;    }    ...private:        friend class __shared_count;        _Sp_counted_base<_Lp>*  _M_pi;};
复制代码
引用计数增长过程

引用计数的增长可以分为下面几种环境:
本质是靠调用_M_add_ref_copy()和_M_add_ref_lock增长的引用计数,详情见__shared_count的源码:
  1. //file: shared_ptr_base.htemplateclass __shared_count{public:        //拷贝构造    __shared_count(const __shared_count& __r) noexcept     : _M_pi(__r._M_pi)    {                if (_M_pi != 0)                        _M_pi->_M_add_ref_copy();    }        //拷贝赋值        __shared_count& operator=(const __shared_count& __r) noexcept    {                _Sp_counted_base* __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;            }                return *this;        }                //转换构造        //weak_ptr利用lock()时会调用此构造函数        explicit __shared_count(const __weak_count& __r)          : _M_pi(__r._M_pi)    {            if (_M_pi != nullptr)                        _M_pi->_M_add_ref_lock();//引用计数++,具体实现依赖于锁计谋              else                        __throw_bad_weak_ptr();    }private:        friend class __weak_count;        _Sp_counted_base<_Lp>*  _M_pi;};
复制代码
弱引用计数的减少过程

弱引用计数的减少可以分为下面几种环境:
本质是靠调用_M_weak_release()减少弱引用计数:
  1. //file: shared_ptr_base.htemplateclass __weak_count{public:        //析构        ~__weak_count() noexcept    {      if (_M_pi != nullptr)        _M_pi->_M_weak_release();    }        //转换赋值        __weak_count& operator=(const __shared_count& __r) noexcept        {            _Sp_counted_base* __tmp = __r._M_pi;        if (__tmp != nullptr)                  __tmp->_M_weak_add_ref();        if (_M_pi != nullptr)                _M_pi->_M_weak_release();        _M_pi = __tmp;            return *this;        }        //拷贝赋值        __weak_count& operator=(const __weak_count& __r) noexcept        {              _Sp_counted_base* __tmp = __r._M_pi;        if (__tmp != nullptr)                  __tmp->_M_weak_add_ref();        if (_M_pi != nullptr)                  _M_pi->_M_weak_release();        _M_pi = __tmp;            return *this;    }    //移动赋值    __weak_count& operator=(__weak_count&& __r) noexcept    {            if (_M_pi != nullptr)                _M_pi->_M_weak_release();              _M_pi = __r._M_pi;            __r._M_pi = nullptr;              return *this;    }private:        friend class __shared_count;        _Sp_counted_base<_Lp>*  _M_pi;};
复制代码
然后在这里对std::weak_ptr::reset()说明一下:它是用来重置 std::weak_ptr 的。调用 reset() 会使std::weak_ptr不再指向它原本观察的对象。
它也会减少原对象的弱引用计数(本质是通过调用的析构函数使得弱引用计数减少)
  1. //file: shared_ptr_base.h
  2. void reset() noexcept
  3. {
  4.         __weak_ptr().swap(*this);
  5. }
复制代码
弱引用计数减为0

在上面提到:弱引用计数的减少是通过调用_M_weak_release()实现的。通太过析_M_weak_release()的代码我们可以知道,_M_weak_release()中主要做了:
  1. //file: shared_ptr_base.h
  2. template<_Lock_policy _Lp = __default_lock_policy>
  3. class _Sp_counted_base : public _Mutex_base<_Lp>
  4. {
  5.     //控制块的弱引用计数为0时,销毁自身
  6.     virtual void _M_destroy() noexcept
  7.     { delete this; }
  8.    
  9.     //弱引用计数--
  10.     //当弱引用计数变为0,销毁控制块
  11.         void _M_weak_release() noexcept
  12.     {
  13.             // Be race-detector-friendly. For more info see bits/c++config.
  14.         _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
  15.         //减少弱引用计数,并返回-1之前的值
  16.             if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
  17.             {
  18.                 _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
  19.                 if (_Mutex_base<_Lp>::_S_need_barriers)
  20.                 {
  21.                 // See _M_release(),
  22.                 // destroy() must observe results of dispose()
  23.                             __atomic_thread_fence (__ATOMIC_ACQ_REL);
  24.                 }
  25.                 _M_destroy();
  26.             }
  27.     }
  28. };
复制代码
引用计数的减少过程

引用计数的减少可以分为下面几种环境:
本质是靠调用_M_release()减少弱引用计数
  1. //file: shared_ptr_base.htemplateclass __shared_count{public:        //析构        ~__shared_count() noexcept        {        if (_M_pi != nullptr)                _M_pi->_M_release();    }    //拷贝赋值    __shared_count& operator=(const __shared_count& __r) noexcept    {                _Sp_counted_base* __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;        }            return *this;        }private:        friend class __weak_count;        _Sp_counted_base<_Lp>*  _M_pi;};
复制代码
引用计数减为0

上面提到:引用计数的减少是通过调用_M_release()实现的。通太过析_M_release()的代码我们可以知道,_M_release()中主要做了
  1. //file: shared_ptr_base.h
  2. template<_Lock_policy _Lp = __default_lock_policy>
  3. class _Sp_counted_base : public _Mutex_base<_Lp>
  4. {
  5.         //当前对象的引用计数为0时,释放管理的资源
  6.     //纯虚函数,取决于释放策略,由派生类实现
  7.     virtual void _M_dispose() noexcept = 0;
  8.     //当前对象的弱引用计数为0时,销毁自身
  9.     virtual void _M_destroy() noexcept
  10.     { delete this; }
  11.        
  12.         void _M_release() noexcept
  13.     {
  14.             // Be race-detector-friendly.  For more info see bits/c++config.
  15.         _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
  16.         //减少引用计数,并返回-1之前的值
  17.         //如果引用计数为0,则释放管理的资源
  18.             if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
  19.             {
  20.                 _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
  21.                 _M_dispose();
  22.                   // There must be a memory barrier between dispose() and destroy()
  23.                   // to ensure that the effects of dispose() are observed in the
  24.                   // thread that runs destroy().
  25.                   // See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
  26.                   if (_Mutex_base<_Lp>::_S_need_barriers)
  27.                   {
  28.                     __atomic_thread_fence (__ATOMIC_ACQ_REL);
  29.                   }
  30.                   // Be race-detector-friendly.  For more info see bits/c++config.
  31.                   _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
  32.                   //减少弱引用计数,并返回-1之前的值
  33.                   //如果弱引用计数为0,则销毁控制块自身
  34.                 if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
  35.                   {
  36.                     _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
  37.                           _M_destroy();
  38.                   }
  39.         }
  40.         }
  41. };
复制代码
这里再说明一下为什么__shared_count要在引用计数减为0时还要对弱引用计数做减1操作:
在__shared_count构造的同时,也会构造一个控制块对象,其中引用计数和弱引用计数一同被初始化为1。这意味着,即使最后一个std::weak_ptr被烧毁了,但若其对应的std::shared_ptr还至少存在一个,那么弱引用计数就不会被减少至0(代码中的注释也是这么提示的)。
  1. //file: shared_ptr_base.h
  2. template<_Lock_policy _Lp = __default_lock_policy>
  3. class _Sp_counted_base : public _Mutex_base<_Lp>
  4. {
  5.         _Atomic_word  _M_use_count;     // #shared
  6.         _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
  7. };
复制代码
在std::shared_ptr对象存在的环境下,所有相干std::weak_ptr对象被烧毁后,控制块仍存在,且其中的弱引用计数为1,此时在烧毁最后一个std::shared_ptr对象时,除了要减少引用计数为0,释放管理的内存资源,还要把最后一个弱引用计数减少为0,烧毁控制块。
在std::weak_ptr对象存在的环境下,所有相干std::shared_ptr对象都被烧毁后,①std::shared_ptr管理的内存资源会被释放(因为引用计数为0,_M_dispose()被调用)②弱引用计数不为0,控制块仍然存在(直到最后一个std::weak_ptr对象被烧毁,控制块才会被烧毁)
参考文章

1.C++2.0 shared_ptr和weak_ptr深入刨析
2.智能指针std::weak_ptr

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4