查看原文
其他

内存管理:具有共享所有权的智能指针(一)

月踏 知知爸爸是码农 2022-06-13

前文分析了具有专属所有权的智能指针,本文继续看一下具有共享所有权语义的智能指针shared_ptr和weak_ptr

一、简介

一句话来讲,shared_ptr的共享所有权的语义是通过引用计数机制来实现的,资源只有一份,引用计数记录了用户个数,当引用计数为0时释放资源,为了方便表达,本文把包含引用计数的数据结构叫做控制块,被管理的资源对象叫做数据块,共享所有权的概念源于多个智能指针可以同时hold同一个数据块,并且通过一个控制块来管理数据块的生命周期,下图是一个不是十分准确但能帮助理解的示意图:

图1

在shared_ptr的实际实现中,比上面图1要复杂的多,下面的图2是标准库实际代码实现的类的关系图,实线表示继承关系,虚线表示包含关系,红色类是智能指针的实现类,绿色类是控制块的实现类:

图2

控制块有三种,对应上面图中绿色部分的三个子类:

  1. 标准控制块,里面只有引用计数和数据块的指针,对应上面图2中的_Sp_counted_ptr类,本文起名叫CBV1(control block version 1)

  2. 带自定义分配器、自定义析构器的控制块,对应上面图2中的_Sp_counted_deleter类,本文起名叫CBV2(control block version 2)

  3. make_shared创建的控制块,这种类型的控制块里面也包含数据块的内容,对应上面图2中的_Sp_counted_ptr_inplace类,本文起名叫CBV3(control block version 3)

二、shared_ptr的引用计数如何更新

引用计数的更新是shared_ptr的关键,从前面图2可以看到shared_ptr的引用计数变量是_Sp_counted_base类中的成员_M_use_count,这是个原子类型变量,在更新时能够保证线程安全。

在创建控制块的时候会对_M_use_count进行初始化为1:

_Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { }

_M_use_count的值在shared_ptr拷贝和赋值时都会变化,区别是拷贝时只会加,而赋值时会有加减两个变化,假设赋值语句为B=A,会对B原来对应的_M_use_count减1,然后对A对应的_M_use_count加1,因为赋值语句既有加又有减,所以更具有代表性,下面仅以赋值操作为主线来追踪展示_M_use_count的更新。首先,在赋值时会调用shared_ptr的赋值构造函数,这个构造函数代码如下:

template<typename _Yp>_Assignable<const shared_ptr<_Yp>&>operator=(const shared_ptr<_Yp>& __r) noexcept { this->__shared_ptr<_Tp>::operator=(__r); return *this;}

可见shared_ptr的赋值构造函数调用了父类__shared_ptr的赋值构造函数,它的内容如下:

template<typename _Yp>_Assignable<_Yp>operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept { _M_ptr = __r._M_ptr; _M_refcount = __r._M_refcount; return *this;}

在上面的__shared_ptr的赋值构造函数里,有_M_refcount的赋值,_M_refcount是__shared_count类型,这又会调用__shared_count的赋值构造函数,它的代码如下:

__shared_count& operator=(const __shared_count& __r) noexcept { _Sp_counted_base<_Lp>* __tmp = __r._M_pi;  if (__tmp != _M_pi) { if (__tmp != nullptr) __tmp->_M_add_ref_copy(); if (_M_pi != nullptr) _M_pi->_M_release(); _M_pi = __tmp; } return *this;}

在这个赋值构造函数中,使用了一个临时变量__tmp,通过它调用_M_add_ref_copy来对__r对应的引用计数加1,再通过调用_M_release对赋值之前_M_pi对应的引用计数减1,_M_add_ref_copy和_M_release的代码如下:

template<> inline void_Sp_counted_base<_S_single>::_M_add_ref_copy(){ ++_M_use_count; }
template<> inline void_Sp_counted_base<_S_single>::_M_release() noexcept {  if (--_M_use_count == 0) { _M_dispose(); if (--_M_weak_count == 0) _M_destroy(); }}

_M_add_ref_copy的代码很简单,只做了加1的操作,_M_release的内容稍微多一些,先对引用计数减1,当等于0时,通过调用_M_dispose来释放数据块占用的空间:

virtual void _M_dispose() noexcept{ delete _M_ptr; }

在shared_ptr的具体实现中,有相当一部分代码是各种各样的构造函数,来应对各种各样的使用场景,上面仅仅展示了通过赋值构造函数来更新shared_ptr的引用计数这一条主线。

三、自定义析构器原理

和unique_ptr一样,shared_ptr也支持自定义析构器,但是又有所不同,如前文具有专属所有权的智能指针所述,unique_ptr中的析构器是类类型的一部分:

template <typename _Tp, typename _Dp = default_delete<_Tp>>class unique_ptr {...};

而shared_ptr是通过构造函数来传入自定义析构器的:

template<typename _Tp>class shared_ptr : public __shared_ptr<_Tp> { ... template<typename _Yp, typename _Deleter, typename = _Constructible<_Yp*, _Deleter>>  shared_ptr(_Yp* __p, _Deleter __d) : __shared_ptr<_Tp>(__p, std::move(__d)) { } ...};

上面代码继续调用了__shared_ptr下面这个构造函数:

template<typename _Tp, _Lock_policy _Lp>class __shared_ptr : public __shared_ptr_access<_Tp, _Lp> { template<typename _Yp, typename _Deleter, typename = _SafeConv<_Yp>>  __shared_ptr(_Yp* __p, _Deleter __d) : _M_ptr(__p), _M_refcount(__p, std::move(__d)) { _M_enable_shared_from_this_with(__p); }};

上面代码初始化了_M_refcount,调用了__shared_count的下面这个构造函数,自定义析构函数被传入了这个构造函数:

template<typename _Ptr, typename _Deleter, typename _Alloc, typename = typename __not_alloc_shared_tag<_Deleter>::type>__shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0) { typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type; typename _Sp_cd_type::__allocator_type __a2(__a); auto __guard = std::__allocate_guarded(__a2); _Sp_cd_type* __mem = __guard.get(); ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a)); _M_pi = __mem; __guard = nullptr;}

上面这段代码是处理自定义析构器的核心代码,创建了CBV2这个版本的控制块,其中__allocate_guarded是定义在allocated_ptr.h中的一个helper function,它调用了allocator_traits<_Alloc>::allocate(...),这里的模板参数_Alloc是所指定的内存分配器,如果用户没有指定,默认情况下它是标准的allocator,它的静态成员函数allocate最终调用了operator new函数在堆上分配了内存,后面的代码使用了前文new and delete介绍的placement new来在前面分配好的内存上创建CBV2这个版本的控制块。

在使用自定义析构器的情况下,当引用计数为0时,就会调到CBV2这个版本控制块中的_M_dispose函数,这个函数内部通过调用自定义析构器完成数据块的释放,这就是shared_ptr的自定义析构器的实现原理。

四、make_shared分析

make_shared是一个helper function,先看下它的定义:

template<typename _Tp, typename... _Args>inline shared_ptr<_Tp> make_shared(_Args&&... __args) { typedef typename std::remove_cv<_Tp>::type _Tp_nc; return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(), std::forward<_Args>(__args)...);}

它又调用了allocate_shared这个helper function:

template<typename _Tp, typename _Alloc, typename... _Args>inline shared_ptr<_Tp>allocate_shared(const _Alloc& __a, _Args&&... __args) { return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a}, std::forward<_Args>(__args)...);}

这里面创建了一个shared_ptr的对象,调用的是shared_ptr的下面这个构造函数:

template<typename _Alloc, typename... _Args>shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args) : __shared_ptr<_Tp>(__tag, std::forward<_Args>(__args)...) { }

这里面又继续调用了__shared_ptr的下面这个构造函数:

template<typename _Alloc, typename... _Args>__shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args) : _M_ptr(), _M_refcount(_M_ptr, __tag, std::forward<_Args>(__args)...){ _M_enable_shared_from_this_with(_M_ptr); }

这里主要对_M_refcount进行了初始化,通过调用下面这个__shared_count构造函数:

template<typename _Tp, typename _Alloc, typename... _Args>__shared_count(_Tp*& __p, _Sp_alloc_shared_tag<_Alloc> __a, _Args&&... __args) { typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type; typename _Sp_cp_type::__allocator_type __a2(__a._M_a); auto __guard = std::__allocate_guarded(__a2); _Sp_cp_type* __mem = __guard.get(); auto __pi = ::new (__mem)_Sp_cp_type(__a._M_a, std::forward<_Args>(__args)...); __guard = nullptr; _M_pi = __pi; __p = __pi->_M_ptr();}

这部分代码很关键,它创建了开始提到的CBV3这种类型的控制块,在这里统一完成了控制块和数据块的内存分配和初始化(_Sp_counted_ptr_inplace中包含数据块成员,同时它又是_Sp_counted_base的一个子类,所以这个类对象同时包含控制块和数据块的内容),上面代码中的__allocate_guarded在上一节讲make_shared的时候已经讲过,在这里它使用了标准中的allocator分配器,allocator的静态成员函数allocate最终调用了operator new函数在堆上分配了内存,在内存分配完成之后,从上面代码可以看出,又调用了在new and delete中讲的placement new来在指定内存空间进行构造了CBV3这种既包含控制块又包含数据块的对象。

总而言之,make_shared会把数据块和控制块的内存空间当成一大块来分配,这样在使用的过程中对cache更加友好,而且只需要调用一次operator new来完成内存分配,这些都对性能的提升有帮助。

五、to be continued

本文是具有共享所有权的智能指针的第一篇,后面还会再写一篇继续深入挖掘具有共享所有权的智能指针的更多细节,敬请期待。


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存