avatar

目录
shared_ptr用法与线程安全性

概念

共享指针,即多个指针指向同一个内存;具体实现方式是采用的引用计数,即这块地址上每多一个指针指向他,计数加一;
引用计数可以跟踪对象所有权,并能够自动销毁对象。可以说引用计数是个简单的垃圾回收体系。

智能指针是模板类而不是指针。创建一个智能指针时,必须指针可以指向的类型,, ……等。
智能指针实质就是重载了->和\操作符的类,由类来实现对内存的管理,确保即使有异常产生,也可以通过智能指针类的析构函数完成内存的释放。

可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器就会递增。当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会递减。一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数析构函数(destructor)来完成销毁工作的。类似于构造函数,每个类都有一个析构函数。就像构造函数控制初始化一样,析构函数控制此类型的对象销毁时做什么操作。shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

完整的例子

c++
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
50
51
52
53
54
55
#include <iostream>
#include <memory>
using namespace std;

struct BigObj {
BigObj() {
std::cout << "big object has been constructed" << std::endl;
}
~BigObj() {
std::cout << "big object has been destructed" << std::endl;
}
};

void test_ref1() {
std::shared_ptr<BigObj> sp1 = std::make_shared<BigObj>();
std::cout << sp1.use_count() << std::endl;

{
std::shared_ptr<BigObj> sp2 = sp1;
std::cout << sp1.use_count() << std::endl;
}
std::cout << sp1.use_count() << std::endl;
BigObj* ptr = sp1.get();

sp1 = nullptr;
std::cout << sp1.use_count() << std::endl;
}

int main()
{
//构建 2 个智能指针
std::shared_ptr<int> p1(new int(10)); //shared_ptr<T> 类模板中,提供了多种实用的构造函数
std::shared_ptr<int> p3 = std::make_shared<int>(10); //
std::shared_ptr<int> p2(p1);
//输出 p2 指向的数据 使用方法例子:可以当作一个指针使用
cout << "p2:" << *p2 << endl;
int *p = p3.get(); //shared_ptr 关联的原始指针
cout << "p3:" << *p << endl;
p1.reset();//引用计数减 1,p1为空指针
if (p1) {
cout << "p1 不为空" << endl;
}
else {
cout << "p1 为空" << endl;
}
//以上操作,并不会影响 p2
cout << *p2 << endl;
//判断当前和 p2 同指向的智能指针有多少个
cout << p2.use_count() << endl;

//
test_ref1();
//我们可以清晰地看到引用计数增加和减少的情况,当减少为 0 的时候就会释放指针对象。把 shared_ptr 设置为 nullptr 就可以让 shared_ptr 去释放所管理的裸指针。 通过 shared_ptr 的 get 方法可以获取它所管理的裸指针。
return 0;
}

析构

析构函数中删除内部原始指针,默认调用的是delete()函数。

Code
1
delete point;

像这样申请的数组,应该调用delete []释放内存,而shared_ptr析构函数中默认delete并不能满足需求。

给shared_ptr添加自定义删除器:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <memory>
using namespace std;

struct BigObj {
BigObj() {
std::cout << "big object has been constructed" << std::endl;
}
~BigObj() {
std::cout << "big object has been destructed" << std::endl;
}
};

void deleter(BigObj *p) {
std::cout << "Custom Deleter\n";
delete[] p;
}
int main() {
std::shared_ptr<BigObj> p(new BigObj[2], deleter);
std::shared_ptr<BigObj> p2(new BigObj[4], [](BigObj *p) { std::cout << "Custom Deleter\n";
delete[] p;});
return 0;
}

误区:

不从 new 的返回值直接构造共享指针

c++
1
2
3
T *a = new T();
shared_ptr<T> ptr1(a);
shared_ptr<T> ptr2(a);

这样的话,ptr1 和 ptr2 的引用计数是单独算的,它们任意一个对象在析构的时候,都会销毁 a 所指的对象,a就为悬空指针,所以,这个对象会被“销毁两次”。因此报错。(make_shared类模板可以避免)
http://c.biancheng.net/view/7898.html
https://www.cnblogs.com/bandaoyu/p/14625038.html

线程安全探究

共享指针的线程安全问题

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

"Multiple threads can simultaneously read and write different shared_ptr objects, even when the objects are copies that share ownership
解释一下以上的说话,比如我们建立一个共享指针管理的对象:

c++
1
shared_ptr<int> ptr1 = make_shared<int>();

此时有两个逻辑块,不应该对待同样的处理。 一个是存储实际值的int,另一个是控制块,它存储使其工作的所有Shared_ptr <>工作原理。
只有控制块本身是线程安全的,也就是共享指针实体可以在不同的线程建立和销毁,共享指针的计数原则都是原子操作,但是如果对指向对象有写操作,那么就需要加锁了,看如下代码

c++
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
shared_ptr<int> global_instance = make_shared<int>(0);

void thread_fcn();

int main(int argc, char** argv)
{
thread thread1(thread_fcn);
thread thread2(thread_fcn);
...
thread thread10(thread_fcn);

chrono::milliseconds duration(10000);
this_thread::sleep_for(duration);

return;
}

void thread_fcn()
{
// This is thread-safe and will work fine, though it's useless. Many
// short-lived pointers will be created and destroyed.
for(int i = 0; i < 10000; i++)
{
shared_ptr<int> temp = global_instance;
}

// This is not thread-safe. While all the threads are the same, the
// "final" value of this is almost certainly NOT going to be
// number_of_threads*10000 = 100,000. It'll be something else.
for(int i = 0; i < 10000; i++)
{
*global_instance = *global_instance + 1;
}
}

综上,共享指针的机制并不能保证多线程可以准确地访问资源,我们还是需要用同步机制比如st::mutex来使其线程安全。使用时我们要明确多个副本访问同一块内存有没有同步的问题

参考文献:
https://ofstack.com/C++/8983/full-analysis-of-shared_ptr-thread-safety.html
https://stackoverflow.com/questions/14482830/stdshared-ptr-thread-safety
https://en.cppreference.com/w/cpp/memory/shared_ptr

文章作者: Sunxin
文章链接: https://sunxin18.github.io/2021/05/14/shared-ptr/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 lalala
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论