📌本文采用wolai制作,原文link: https://www.wolai.com/ravenxrz/w8mFh9W9xfoz12i5ENRv74
前文介绍了 std::unique_ptr,本文继续分析另一个常用的智能指针: std::shared_ptr
1 TODO·
- [ ] std::shared_ptr析构释放资源时的 fence的作用
- [ ] std::enable_shared_from_this 中的 week_ptr构造详细分析
2 预计解答的问题·
std::shared_ptr
操作是否为线程安全
📌即使从拷贝构造函数看,拷贝构造中的
_M_ptr
和_M_refcount
赋值是分开的,所以拷贝构造一个shared_ptr
不是线程安全的。但是_M_refcount
本身的加减是原子的,内部是一个原子变量。
std::shared_ptr
内存空间占用
📌见:内存占用, 简单总结, sizeof(std::shared_ptr) =16B。 但即使是最简单的 raw pointer构造出来的std::shared_ptr,总内存占用也至少是40B。如果是
std::make_shared
或者有自定义的deleter构造出来的,内存占用可能更多。
- 使用
raw_pointer
来构造std::shared_ptr
和make_shared
的区别
📌make_shared和直接raw pointer相比,区别在于内存分配上: 1. make_shared对象的管理块(引用计数,allocator)和对象本身的内存分配只有一次 2. raw_pinter首先要分配需要管理的对象,再分配对象管理块(引用计数,deleter)
-
enable_shared_from_this
实现原理 -
enable_shared_from_this, 本质上就是一个 private 的
weak_ptr
-
这个
weak_ptr
的初始化由shared_ptr
构造时初始化(而不是继承它的类型对象初始化),初始化时,weak_ptr
控制块指向shared_ptr
的控制块,且weak_ptr
也持有要管理的指针。 -
当调用
shared_from_this
时,通过这个weak_ptr
来构造一个shared_ptr
,这个新的shared_ptr
就能够共享原来最初的控制块。
3 类图·
classDiagram class __shared_ptr~_Tp~ { // enable_shared_from_this 在本类处理 _M_enable_shared_from_this_with() element_type* _M_ptr; // Contained pointer. __shared_count<_Lp> _M_refcount; // Reference counter. } class shared_ptr~_Tp~ class __shared_ptr_access~_Tp, _Lp=__default_lock_policy~ { // 定义指针操作 *, -> // 如果指向的是数组,还定义了[]访问 } class __shared_count~_Lp = __default_lock_policy~ { // 引用计数 _Sp_counted_base<_Lp>* _M_pi; } class _Sp_counted_base~_Lp = __default_lock_policy~ { // 引用计数具体的实现类 - _Atomic_word _M_use_count; // #shared - _Atomic_word _M_weak_count; // #weak + (#shared != 0) } class _Mutex_base~_Lp~ { // 空类,如果模板是_S_mutex, 则定义`_S_need_barriers`为1 // 否则该值为0 } class _Sp_counted_ptr~_Ptr, _Lock_policy _Lp~{ // 裸指针类型管理 _Ptr _M_ptr; // 管理指向的对象的资源释放 } class _Sp_counted_ptr_inplace~ _Tp, _Alloc, _Lp~ { // make_shared构造的类型, 对allocator 有EBO优化 _Impl _M_impl: 管理指向的对象的资源分配与释放 } class _Sp_counted_deleter~ _Ptr, _Deleter, _Alloc, _Lp~ { // 自定义deleter或者为数组类型指针 // deleter和std::tuple一样,也使用了EBO _Impl _M_impl: 管理指向的对象的资源释放 } _Mutex_base <|-- _Sp_counted_base __shared_ptr~_Tp~ <|-- shared_ptr~_Tp~ __shared_ptr_access~_Tp, _Lp~ <|-- __shared_ptr~_Tp~ __shared_count~_Lp = __default_lock_policy~ --o __shared_ptr~_Tp~ _Sp_counted_base <|--_Sp_counted_deleter _Sp_counted_deleter -- __shared_count: 数组类型、自定义deleter _Sp_counted_base<|-- _Sp_counted_ptr _Sp_counted_ptr -- __shared_count: 可直接调用delete来析构的类型 _Sp_counted_base<|-- _Sp_counted_ptr_inplace _Sp_counted_ptr_inplace -- __shared_count: make_shared构造的类型
4 源码·
shared_ptr 定义如下:
1 | template <typename _Tp> class shared_ptr : public __shared_ptr<_Tp> { ... }; |
shared_ptr
本身没有成员变量,实现主要看其父类 __shared_ptr
4.1 __shared_ptr
·
定义如下:
1 | // Forward declarations. |
明显有一个指向要管理的对象的指针_M_ptr
, 对象类型如果是数组类型,则执行decay。
remove_extent: If
T
is an array of some typeX
, provides the member typedeftype
equal toX
, otherwisetype
isT
. Note that if T is a multidimensional array, only the first dimension is removed.
另一个成员 _M_refcount
是对象的引用计数,稍候分析。
先继续往下看父类:__shared_ptr_access
4.1.1 __shared_ptr_access
·
本类提供了指针类型的操作符,如 *,->, []。
定义如下:
1 | // Define operator* and operator-> for shared_ptr<T>. |
4.1.2 __shared_count
·
__shared_count
是引用计数的实现类。
定义如下:
1 | template <_Lock_policy _Lp = __default_lock_policy> class __shared_count; |
持有一个_Sp_counted_base
成员,本类只是一个包装类,引用计数还在 _Sp_counted_base
中。本类是std::shared_ptr
的核心类,详细分析下。
4.1.2.1 _Sp_counted_base
·
实现:
1 | template <_Lock_policy _Lp = __default_lock_policy> |
找到了原子引用计数的地方。一共有两个原子计数。
4.1.2.2 构造函数1—raw pointer构造·
可先看构造函数: 使用raw pointer构造, 不带自定义deleter, 再回头看此处。
使用raw pointer构造时,会有如下构造_M_refcount(__p, typename is_array<_Tp>::type())
1 | template <typename _Yp, typename = _SafeConv<_Yp>> |
此时转入 __shared_count
构造
1 | template <typename _Ptr> |
对于非arrary的类型, 调用:
1 | template<typename _Ptr> |
此处的_Sp_counted_ptr
:
1 | // Counted ptr with no deleter or allocator support |
对于arrary类型,调用含有deleter的构造函数,用于后续析构:
1 | // The default deleter for shared_ptr<T[]> and shared_ptr<T[N]>. |
利用placement new,把管理的pointer和deleter放在一片内存上。
数组类型默认的deleter为:__sp_array_delete
, 该类为空类,可以使用EBO(EBO见下文)。
此处的_Sp_counted_deleter
实现为:
1 | // Support for custom deleter and/or allocator |
这个类也是一个包装类,内部实现为_Impl
,继承了_Sp_ebo_helper
, 这个用于实现空基类优化(EBO)。
4.1.2.3 构造函数2 — make_shared 构造·
可先看 构造函数:make_shared
,再回头看此处。
1 | template<typename _Tp, typename _Alloc, typename... _Args> |
和上文的 arrary类型构造有些类似。 利用 placement new
将管理的对象和allocator构造在一起。但是不同的点在于: 这里的placement new
还原地构造了要管理的对象本身,而上文arrary类型只是分配了要管理对象的指针,而不是对象本身。 最后注意最后一样代码的赋值。这保证__shared_ptr类中的成员被正确赋值。
📌make_shared和直接raw pointer相比,区别在于内存分配上: 1. make_shared对象的管理块(引用计数,allocator)和对象本身的内存分配只有一次 2. raw_pinter首先要分配需要管理的对象,再分配对象管理块(引用计数,deleter)
再看下这里依赖的 _Sp_counted_ptr_inplace
类:
1 | template <typename _Tp, typename _Alloc, _Lock_policy _Lp> |
和 _Sp_counted_deleter结构基本完全一致。除了Impl
内的成员不同,一个是裸指针,一个是__aligned_buffer
。 __aligned_buffer
确保了 _Tp
类型对象在内存中的对齐。这对于 _Tp
类型是某些向量类型或需要特定内存对齐的其他数据结构的情况至关重要。
4.1.2.4 拷贝构造·
1 | __shared_count(const __shared_count &__r) noexcept : _M_pi(__r._M_pi) { |
此处做了拷贝ref,同时加了引用计数。
1 | void _M_add_ref_copy() { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); } |
注意这里只加了 _M_use_count
的计数。
4.1.2.5 析构·
__shared_count
析构如下:
1 | ~__shared_count() noexcept { |
_M_pi
是 _Sp_counted_base
类型,release函数如下:
1 | void _M_release() noexcept { |
这段代码有三个重要的点:
_M_dispose
虚函数,由三个子类实现_M_destroy
虚函数,由三个子类实现__atomic_thread_fence
保证_M_dispose
的效果一定被_M_destroy
观察到。这涉及到多线程并发安全问题。
逻辑上来看,是先减强引用,如果强引用减为了1, 再减弱引用。
先看前两个点:
_Sp_counted_ptr
直接管理raw pinter:
1 | virtual void _M_dispose() noexcept { delete _M_ptr; } |
先释放要管理的对象,再释放管理控制块本身。
_Sp_counted_deleter
管理数组类型或自定义deleter类型:
1 | virtual void _M_dispose() noexcept { _M_impl._M_del()(_M_impl._M_ptr); } |
获取自定义deleter,析构对象(控制块的内存释放由allocator控制,所以只是析构对象)。
_Sp_counted_ptr_inplace
管理std::make_shared
构造的对象:
1 | virtual void _M_dispose() noexcept { |
由于std::make_shared
出来的对象和控制块都是由allocator
分配的,所以这里都只是调用其析构函数,而不是直接delete。
📌先跳过第三点,目前没完全理解.
现在再来看看第三点:
__atomic_thread_fence
保证_M_dispose
的效果一定被_M_destroy
观察到。这涉及到多线程并发安全问题。
要理解这个问题:
摘抄自公司内某位大佬的发言:多线程环境下一组读写操作不能保证得到一个基于发生时间的全序,其实对同一个地址的一组写操作是有一个唯一的全序的,但是在这里我们需要的操作(也是fetch_sub提供的)同时包含了读和写:fetch_sub做的是read modify write,然后返回的是它read到的值。我们需要在返回值为1的时候去做delete操作。当这个智能指针关联的对象被不止一个线程持有时,会有多个线程做fetch_sub,那么fetch_sub读到的值就会是其他fetch_sub写入的值。这时候synchronize-with就需要出场了,因为我们需要保证当一个fetch_sub读到1时(记这个操作为B),写入这个1的线程里在执行fetch_sub(记这个操作为B)前的所有写操作都happens before A。不然就会出现内存已经被释放了还在对它操作的错误。所以在做fetch_sub的时候是要求要用acq-rel内存序的。但是fetch_add用relax就可以了
考虑如下case, thread 1为最终减到1的thread, thread 2为减到2的 thread。thread 1最终释放资源。
1 |
|
1 | void thread2(shared_ptr<T> ptr) { |
如果使用relaxed order,thread 2可重排为:
1 | void thread2(shared_ptr<T> ptr) { |
参考link:
https://www.reddit.com/r/cpp_questions/comments/19bbatx/is_it_safe_to_relax_memory_order_for_shared_ptr/
4.2 内存占用·
本节如有错误,请帮忙指正。
到这里,结合 类图可以得到 std::shared_ptr
占用的内存大小。 (注:这里并不说sizeof(std::shared_ptr)的大小,而是包含heap内存申请的大小),另外这里只说通过 raw pointer构造的std::shared_ptr的内存占用。
std::shared_ptr 继承自其它类,有一个虚函数指针。 8B(std::shared_ptr的父类std::_ _shared_ptr并没有声明任何virtual函数,这一度让我感到意外,后来想了想,std::shared_ptr的使用中并不存在多态释放,也就是说A *a = new B
,delete a
(B是A的子类)这种case, 管理对象的释放都有对应的deleter,
而自己本身的释放,可以直接释放,所以也就不需要virtual了)- 有一个指向要管理的对象的指针。 8B
- 有一个指向控制块的指针。 8B
第2+第3就是 sizeof(std::shared_ptr)的结果,即16B
- 控制块至少包含4字节的强引用原子变量和4字节的弱引用原子变量。 8B
- 控制块是多态的,有一个虚函数指针。 8B
- 控制块是
_Sp_counted_ptr
类型,还有一个指向要管理的对象的指针。 8B
所以即使是最简单的_Sp_counted_ptr
类型,内存占用至少是 40B。
如果是std::make_shared
或者有自定义的deleter,内存占用可能更多。
4.3 常用函数解析·
4.3.1 构造函数: 使用raw pointer构造, 不带自定义deleter·
1 | /** |
转调用父类构造.
1 | template <typename _Yp, typename = _SafeConv<_Yp>> |
有两个关注点:
1.是_M_refcount
的构造函数, 也即 __shared_count
的构造函数,前文已经分析。
2.是_M_enable_shared_from_this_with
实现: 下文分析。
4.3.2 构造函数+operator=:从另一个shared_ptr
构造或赋值·
1 | /** |
分开看,先看构造函数:
拷贝构造:
1 | template <typename _Yp, typename = _Compatible<_Yp>> |
赋值要管理的指针对象,同时构造引用计数。
引用计数的拷贝构造前文已经介绍过: 见拷贝构造
📌即使从拷贝构造函数看,拷贝构造中的
_M_ptr
和_M_refcount
赋值是分开的,所以拷贝构造一个shared_ptr
不是线程安全的。但是_M_refcount
本身的加减是原子的,内部是一个原子变量。
再看移动构造:
1 | __shared_ptr(__shared_ptr &&__r) noexcept : _M_ptr(__r._M_ptr), _M_refcount() { |
同样是赋值管理对象,并擦除rhs的管理指针。初始化_M_refcount
,并与rhs交换。 这个过程同样不是原子的,也就不是线程安全的。
1 | // __shared_count成员函数 |
此处交换了__shared_count
唯一的成员变量指针,该指针指向了要管理的对象和deleter。
4.3.3 构造函数:由week_ptr构造·
除了raw pointer和 拷贝、移动构造以外,还可以从weak_ptr
构造:
1 | /** |
转到__shared_ptr
1 | // This constructor is used by __weak_ptr::lock() and |
拿到week
的_M_refcount
构造 shared_ptr的_M_refcount
。 同时初始化_M_ptr
指针。
todo(zhangxingrui): 分析
weak_ptr
的引用计数。
4.3.4 构造函数:make_shared
·
如下是make_shared
源码:
1 | /** |
接收变参模板,使用完美转发构造。转到函数 std::allocate_shared
1 | /** |
这又切到shared_ptr
的另一个构造函数:
1 | template <typename _Yp, typename _Alloc, typename... _Args> |
allocate_shared
被声明为友元函数,所以才能访问private的特殊构造函数, 同时转发到父类的构造函数:
1 | // This constructor is non-standard, it is used by allocate_shared. |
将_M_ptr
初始化(先置空),转到_M_refcount
构造。这部分已在 构造函数2 — make_shared 构造分析。 注意,将成员变量_M_ptr
转到了_M_refcount
的构造中,又会进一步赋值。
4.3.5 析构函数·
shared_ptr
析构,转到父类__shared_ptr
析构:
1 | ~__shared_ptr() = default; |
也即自然析构成员变量,主要关注__M_refcount
(__shared_count
类型)析构。
见 析构。
4.4 enable_shared_from_this
原理·
std::enable_shared_from_this
主要用于解决在类成员函数中安全地获取 shared_ptr
指向自身实例的问题。 直接使用 shared_ptr<MyClass>(this)
是不安全的,因为它会导致创建第二个控制块,最终可能导致双重释放。enable_shared_from_this
提供了一种安全的方式来避免这个问题。
举个例子:
1 |
|
现在来看下enable_shared_from_this
的源码:
1 | /** |
整个类很简单,具有唯一的成员_M_weak_this
。
重点看下如下函数:
1 | shared_ptr<_Tp> shared_from_this() { |
如何用一个weak_ptr
构造shared_ptr
前文已经介绍过,见构造函数:由week_ptr构造
现在的问题是,_M_weak_this
是怎么初始化的?回到介绍 __shared_ptr
的构造函数:
1 | template <typename _Yp, typename = _SafeConv<_Yp>> |
4.4.1 __shared_ptr::_M_enable_shared_from_this_with
实现·
_M_enable_shared_from_this_with
有两个实现,条件是__has_esft_base
type trait是否生效。也即检测传入_Yp
类型是否继承自std::enable_shared_from_this
, 如果继承,则实现为:
1 | template <typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type> |
如果继承自enable_shared_from_this
, 则转为enable_shared_from_this
对象,然后执行__base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
,
赋值函数如下:
1 | // 如下函数在 enable_shared_from_this 中 |
_M_assign
函数如下:
1 | // Used by __enable_shared_from_this. |
(这是一个双link) 到这里为止。 我们能够知道:
- enable_shared_from_this, 本质上就是一个 private 的
weak_ptr
- 这个
weak_ptr
的初始化由shared_ptr
构造时初始化(而不是继承它的类型对象初始化),初始化时,weak_ptr
控制块指向shared_ptr
的控制块,且weak_ptr
也持有要管理的指针。 - 当调用
shared_from_this
时,通过这个weak_ptr
来构造一个shared_ptr
,这个新的shared_ptr
就能够共享原来最初的控制块。
如果一个类型继承了enable_shared_from
, 但是它的对象没有用std::shared_ptr
,那么直接从这个对象share_from_this
会抛异常:
1 | class A : public std::enable_shared_from_this<A> { |
回到__shared_ptr构造函数,
如果传入类型没有继承enable_shared_from_this
,则_M_enable_shared_from_this_with
函数为空:
1 | template <typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type> |
5 总结·
本文详细分析了std::shared_ptr
的实现原理。从4个问题引入。逐渐分析std::shared_ptr
的继承关系,探讨了std::shared_ptr
的常用函数实现,特别是构造函数引出的3种控制块(raw pointer构造,自定义deleter构造和std::make_shared),讨论了std::shared_ptr
的线程安全性、内存空间占用、raw pointer构造和std::make_shared
构造的区别。 最后还详细分析了 std::enable_shared_from_this
的实现原理。