智能指针是线程安全的?

Last updated on 8 months ago

智能指针实现原理?

智能指针(Smart Pointers)是一种用于管理动态分配的内存资源的 C++ 类模板,不是指某个关键字,确保在不再需要时自动释放所持有的资源,从而避免内存泄漏和悬空指针等内存管理问题; 这个类里面通过引用计数 来跟踪指向同一对象的指针数量,每当一个智能指针指向对象时,对象的引用计数会增加;当智能指针超出作用域或被销毁时,引用计数会减少。当引用计数为零时,表示没有指针指向对象,可以安全地释放对象的内存。

  • std::shared_ptr:共享式智能指针,允许多个指针共享同一个资源,这个资源可以被其他shared_ptr 引用,所以需要使用引用计数来管理资源的生命周期。
  • std::unique_ptr:独占式智能指针,不能被复制,只能移动所有权,由于unique_ptr是独占所指向对象的所有权,不能共享,所以不使用引用计数,使用独占性质来管理对象的生命周期。
  • std::weak_ptr:弱引用智能指针,可以解决 std::shared_ptr 的循环引用问题,不会增加资源的引用计数,不会延长资源的生命周期。

智能指针是线程安全吗?

先说下什么是线程安全?

线程安全指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的公用变量,使程序功能正确完成;

举一个线程操作不安全的例子:

10个线程,对global_counter 自加,期望值是 200000 ;
我们都知道类似 i++的操作,底层涉及了 读取、修改和写回多个步骤,所以这 ++操作 并不是线程安全的;

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
#include <stdio.h>
#include <pthread.h>

#define NUM_THREADS 10
#define ITERATIONS 10000

int global_counter = 0;

void *thread_function(void *arg) {
for (int i = 0; i < ITERATIONS; ++i) {
global_counter++;
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
// 创建多个线程
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_create(&threads[i], NULL, thread_function, NULL);
}
// 等待所有线程完成
for (int i = 0; i < NUM_THREADS; ++i) {
pthread_join(threads[i], NULL);
}
printf("Global counter value: %d\n", global_counter);

return 0;
}

结果为 217250,和预期的 200000 不一致

1
2
3
4
root@ubuntu:/home# gcc  thread.c -o thread -pthread
root@ubuntu:/home# ./thread
Global counter value: 217250
root@ubuntu:/home#

智能指针类的里面的关键操作

从智能指针的作用来看,只是管理内存的生命周期,并不负责竞争和同步问题

从智能指针内部类实现来看,最为关键是 引用计数的自增,多线程场景,使用shared_ptr,这个时候是有可能? 为了确定下,直接用gdb 进去看下(我猜测时在 拷贝构造的时候会调用计数+1操作)

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
#include <iostream>
#include <memory>
#include <vector>

int main() {
// 创建一个 std::shared_ptr,并将其初始化为指向一个动态分配的整数数组
std::shared_ptr<std::vector<int>> ptr1 = std::make_shared<std::vector<int>>(5, 10); // 创建一个包含5个元素的整数数组

// 创建另一个 std::shared_ptr,指向相同的对象
std::shared_ptr<std::vector<int>> ptr2 = ptr1;

// 打印每个 shared_ptr 的引用计数
std::cout << "ptr1 引用计数: " << ptr1.use_count() << std::endl;
std::cout << "ptr2 引用计数: " << ptr2.use_count() << std::endl;

// 创建第三个 shared_ptr,指向相同的对象 //断点打在行!
std::shared_ptr<std::vector<int>> ptr3 = ptr1;

// 打印每个 shared_ptr 的引用计数
std::cout << "ptr1 引用计数: " << ptr1.use_count() << std::endl;
std::cout << "ptr2 引用计数: " << ptr2.use_count() << std::endl;
std::cout << "ptr3 引用计数: " << ptr3.use_count() << std::endl;
return 0;
}

17行打两个断点,

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
(gdb) 
main () at shareptr.cc:13
13 std::cout << "ptr1 引用计数: " << ptr1.use_count() << std::endl;
(gdb)
ptr1 引用计数: 2
14 std::cout << "ptr2 引用计数: " << ptr2.use_count() << std::endl;
(gdb)
ptr2 引用计数: 2

# step 进入
17 std::shared_ptr<std::vector<int>> ptr3 = ptr1;
(gdb) s
std::shared_ptr<std::vector<int, std::allocator<int> > >::shared_ptr (
this=0x7fffffffe490) at /usr/include/c++/7/bits/shared_ptr.h:119
119 shared_ptr(const shared_ptr&) noexcept = default;

#拷贝构造函数的默认声明,使用默认实现,
# step 再进入
(gdb) s
std::__shared_ptr<std::vector<int, std::allocator<int> >, (__gnu_cxx::_Lock_policy)2>::__shared_ptr (this=0x7fffffffe490)
at /usr/include/c++/7/bits/shared_ptr_base.h:1121
1121 __shared_ptr(const __shared_ptr&) noexcept = default;

# step 再进入,终于可能到实现函数了
(gdb) s
std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count (
this=0x7fffffffe498, __r=...)
at /usr/include/c++/7/bits/shared_ptr_base.h:688
688 : _M_pi(__r._M_pi)

#next 逐步看
(gdb) n
690 if (_M_pi != 0)
# 打印发现有用的信息, _M_use_count = 2 ,符合当前的 引用计数
(gdb) p *_M_pi
$2 = {<std::_Mutex_base<(__gnu_cxx::_Lock_policy)2>> = {<No data fields>},
_vptr._Sp_counted_base = 0x555555758c80 <vtable for std::_Sp_counted_ptr_inplace<std::vector<int, std::allocator<int> >, std::allocator<std::vector<int, std::allocator<int> > >, (__gnu_cxx::_Lock_policy)2>+16>,
_M_use_count = 2, _M_weak_count = 1}

#next 逐步看,发现add
(gdb) n
691 _M_pi->_M_add_ref_copy();
# step 继续深入,发现时 c++ 的atomic操作, 这个就是原子操作
(gdb) s
std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_add_ref_copy (
this=0x55555576be70) at /usr/include/c++/7/bits/shared_ptr_base.h:138
138 { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }


从代码上看,引用计数累计时原子操作,所以说明 shared_ptr 内部操作是线程安全的,但是不能保证多线程场景下用 shared_ptr 管理内存数据的就一定是线程安全性