最近阅读了 C++ 智能指针的部分实现源码,简单总结和记录一下 std::share_ptr/std::weak_ptr 内部结构和工作原理。
1. std::shared_ptr
1.1. 概念
std::shared_ptr 是 C++11 引入的一种智能指针,它可以用来自动管理对象的生命周期,以防止内存泄漏。
1.2. 结构
1.2.1. 常规创建对象
1
2
3
4
5
6
7
8
class A {
public:
std::string m_str;
A(const char* s) : m_str(s) {}
~A() {}
};
auto a = std::shared_ptr<A>(new A("hello"));
std::shared_ptr 的内部结构并不复杂,关键的两个成员指针:
- _M_ptr:数据块指针。
- _M_pi:控制块指针,控制块里面有
引用计数
和弱引用计数
。
1
2
3
4
5
6
|-- shared_ptr
|-- element_type* _M_ptr; # 数据块指针。
|-- __shared_count<_Lp> _M_refcount; # 引用计数对象。
|-- _Sp_counted_base<_Lp>* _M_pi; # 控制块指针。
|-- _Atomic_word _M_use_count; # 引用计数。
|-- _Atomic_word _M_weak_count; # 弱引用计数。
1.2.2. make_shared 创建对象
1
auto a = std::make_shared<A>("hello");
使用 std::make_shared 创建 std::shared_ptr 对象更高效:
- 因为 std::make_shared 参数是个 万能引用,可以有效防止数据拷贝。
-
元素对象 A,可以在 std::shared_ptr 内部进行构造,可以实现更多的优化,例如:std::shared_ptr 内部创建了一块连续的内存空间,在这块内存上对
控制块
和数据块
对象进行构造。- 使用连续内存空间,可以有效减少内存碎片。
- 减少 new 操作次数,本来两个块需要分别调用 new 分配两次内存空间,现在只需要一次。
- 将
控制块
和数据块
连接在一起,系统访问连续内存空间要比访问离散的更高效。(重载的 new 操作符,在自由存储区(连续内存空间)上构造对象 A)。
- 成员结构。
1
2
3
4
5
6
7
8
9
10
11
12
13
|-- shared_ptr
|-- element_type* _M_ptr; ---------------------------------------+
|-- __shared_count<_Lp> _M_refcount; |
|-- _Sp_counted_base<_Lp>* _M_pi; --------------------------+ |
| |
# 控制块实例 | |
|-- _Sp_counted_ptr_inplace : public _Sp_counted_base<_Lp> <----+ |
|-- _Sp_counted_base<_Lp>* _M_pi; |
|-- _Atomic_word _M_use_count; |
|-- _Atomic_word _M_weak_count; |
|-- _Impl _M_impl |
|-- __gnu_cxx::__aligned_buffer<_Tp> _M_storage; |
|-- unsigned char __data[_Len]; <---------------------------+
- 内部内存分配。(有兴趣的朋友可以研读源码,这里不详细展开了。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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_counted_ptr_inplace 分配内存。
_Sp_cp_type* __mem = __guard.get();
// 在连续的内存空间 __mem 上构建数据块和控制块。
auto __pi = ::new (__mem)
_Sp_cp_type(__a._M_a, std::forward<_Args>(__args)...);
__guard = nullptr;
// __shared_count::_M_pi 指向控制块。
_M_pi = __pi;
// shared_ptr::_M_ptr 指向数据块。
__p = __pi->_M_ptr();
}
1.3. 引用计数
std::shared_ptr 通过引用计数维护共享对象实体的生命周期:
- 当一个新的 shared_ptr 指向一个对象,该对象的引用计数就会增加。
- 当一个 shared_ptr 被销毁或者指向另一个对象,原来的对象的引用计数就会减少。
- 当引用计数变为 0 时,对象就会被自动删除。
1.3.1. 增加引用计数
每当一个新的 shared_ptr 指向一个对象,该对象的引用计数就会增加。
内部通过原子操作维护 _M_use_count 引用计数,保证引用计数在多线程环境下安全工作。
- 增加引用计数流程。
1
2
3
4
5
6
|-- main
|-- shared_ptr(const shared_ptr&) noexcept = default;
|-- __shared_count(const __shared_count& __r)
|-- _M_pi->_M_add_ref_copy();
|-- __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1)
|-- __atomic_add(__mem, __val);
1.3.2. 减少引用计数
std::shared_ptr 对象生命期结束时,如果引用计数为零,那么销毁元素对象。
- 引用计数:_M_use_count == 0 销毁
数据块
。 - 弱引用计数:_M_weak_count == 0,销毁
控制块
。
关于 弱引用 下文将会讲述。
- 析构流程。
1
2
3
4
5
6
7
8
|-- main
|-- ~__shared_ptr() = default;
|-- _M_pi->_M_release();
|-- if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
|-- _Sp_counted_base::_M_dispose(); # 销毁数据块。
# 【注意】在弱引用计数为 0 才会销毁控制块。
|-- if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
|-- _Sp_counted_base::_M_destroy(); # 销毁控制块。
- 内部实现源码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <typename _Tp>
class shared_ptr : public __shared_ptr<_Tp> {
public:
...
protected:
~__shared_count() noexcept {
if (_M_pi != nullptr)
_M_pi->_M_release();
}
...
};
template <_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp> {
...
public:
void
_M_release() noexcept {
// 原子操作。
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) {
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
// 销毁数据块。
_M_dispose();
...
_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);
// 销毁控制块。
_M_destroy();
}
}
}
...
};
2. std::weak_ptr
2.1. 概念
std::weak_ptr 是 C++11 中引入的另一种智能指针。
作用:
- 它的主要用途是防止 std::shared_ptr 的
循环引用问题
,生命期结束后,没有自动销毁元素对象。 - std::weak_ptr 不会增加 std::shared_ptr 所指向对象的 引用计数。(具体请参考上面描述的 数据块 和 控制块 销毁的时机)。
- std::weak_ptr 通常用于观察 std::shared_ptr。如果 std::weak_ptr 所指向的对象还存在的话,可以通过 std::weak_ptr::lock() 来创建一个新的 std::shared_ptr,否则这个新的 std::shared_ptr 就会是空的。
参考下面 Demo,循环引用问题导致元素对象没有释放。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// g++ -std=c++11 test.cpp -o t && ./t
#include <iostream>
#include <memory>
class B;
class A {
public:
A() { std::cout << "A()\n"; }
~A() { std::cout << "~A()\n"; }
std::shared_ptr<B> m_obj;
};
class B {
public:
B() { std::cout << "B()\n"; }
~B() { std::cout << "~B()\n"; }
std::shared_ptr<A> m_obj;
};
int main() {
auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->m_obj = b;
b->m_obj = a;
return 0;
}
// 输出:
// A()
// B()
2.2. 结构
std::weak_ptr 的内部成员结构与 std::shared_ptr 有惊人相似,原理大同小异。
1
2
3
4
5
6
7
|-- weak_ptr
|-- __weak_ptr
|-- _Tp* _M_ptr; # 数据块指针。
|-- __weak_count<_Lp> _M_refcount; # 引用计数对象。
|-- _Sp_counted_base<_Lp>* _M_pi; # 控制块指针。
|-- _Atomic_word _M_use_count; # 引用计数。
|-- _Atomic_word _M_weak_count; # 弱引用计数。
3. 线程安全
3.1 问题
问:std::shared_ptr 对象是否线程安全?!
答:不安全!
3.2 分析
1
2
3
4
|-- shared_ptr
|-- element_type* _M_ptr; # 数据块指针。
|-- __shared_count<_Lp> _M_refcount; # 引用计数对象。
|-- _Sp_counted_base<_Lp>* _M_pi; # 控制块指针。
- 数据块指针:_M_ptr,std::shared_ptr 内部并没有任何同步原语对它进行保护,多线程环境下读写,不安全!
-
引用计数和弱引用计数是原子操作,它们是安全的;但是原子操作保护的区域有限,多线程环境下引用计数为 0 时,销毁对象不安全。
请看下图步骤:
- A 线程执行步骤一。
- B 线程执行骤二:释放 _M_pi 指向的控制块内存。
- A 线程执行步骤三还安全吗?!
3.3 小结
以上分析可见:多线程并发场景下直接使用共享的 std::shared_ptr 变量并非线程安全;得 上锁
,让它呆在线程安全的临界区内才能保证安全使用。
- std::shared_ptr 对象本身不线程安全。
- std::shared_ptr
管理的 T 元数据也不线程安全。