为什么多线程读写shared_ptr要加锁?

C++ 智能指针使用和原理分析

本文章主要介绍如下内容:

  • C++11 支持的三种不同类型的智能指针:shared_ptrweak_ptr以及unique_ptr的区别;
  • 探究std::shared_ptr底层实现,以及make_shared的优势;
  • 分析智能指针不恰当使用导致的问题:double free、内存泄漏和线程安全

智能指针基本介绍

shared_ptr

shared_ptr体现的是共享所有权,即多个shared_ptr共享所指向对象的所有权,当且仅当所有的shared_ptr均被析构时,所指向的对象才会被析构释放。

观察如下代码,在shared_ptr构造、赋值、退出作用域时,会对引用计数use_count进行加一、减一。当减为0时,会调用delete析构并释放所指向的对象:

void test() {
  std::shared_ptr<Widget> pw1(new Widget); // use_count = 1
  std::shared_ptr<Widget> pw2 = pw1; // use_count = 2
  // ...
  // 退出pw1和pw2所在作用域,pw1,pw2先后被析构,use_count减为0,调用delet析
  // 构并释放所指向的Widget对象
}

在 C++11 时,尚且只支持对象类型,只能通过自定义删除器,来实现对数组的支持。而在 C++ 17 之后,便支持了数组类型。

// 方法一
std::shared_ptr<int> sp3(new int[10](),[](int *p){ delete []p; });
// 方法二:C++17之后
std::shared_ptr<int[]> sp3(new int[10]());
int i = sp3[2];

weak_ptr

weak_ptr不负责对象的生命周期管理,它指向一个由shared_ptr管理的对象,但不会改变引用计数。通过lock()来判断所指的对象是否存在,若存在则返回一个shared_ptr,否则为nullptr

weak_ptr的引入主要是为了解决循环引用的问题。如下图所示,A中有一个shared_ptr指向B,B中也有一个shared_ptr指向A。这会导致use_count始终不为0,A和B对象始终无法被析构并释放。解决办法,就是将其中的一个指针改为weak_ptr,来破环这种循环引用。

在这里插入图片描述

unique_ptr

不同于shared_ptr、weak_ptrunique_ptr独占所指对象的所有权。从某种程度上了说它实现了 RAII 语义,并且零开销。所以如果有独占所指对象的所有权语义的场景,均可用unique_ptr而不用担心性能损失。

以下是一个基本示例:

// 创建并初始化一个unique_ptr
std::unique_ptr<Widgt> pw = std::make_unique<Widgt>();
// 将所有权移交给pw2
std::unique_ptr<Widgt> pw2 = std::move(pw);

shared_ptr的实现原理

shared_ptr是引用计数型智能指针。它包含两个成员指针,一个指向所管理的对象ptr,另一个指向堆上的引用计数块ref_count

观察如下的结构示意图。可以观察到ref_count中有多个成员,其中use_count表示有多少个shared_ptr指向对象Foo,weak_count表示有多少个shared_ptr or weak_ptr指向对象ref_count

在这里插入图片描述

这里只分析shard_ptruse_count。故示意图可以简化为如下所示:

在这里插入图片描述

如果再执行shared_ptr<Foo> y = x;,那么对应的数据结构如下:

在这里插入图片描述

这里可以分为两步:

  • 第一步,复制ptr指针,这一步是atomic的。
    在这里插入图片描述

  • 第二步,复制cnt指针,增加use_count,这一步也是atomic的。
    在这里插入图片描述

为什么要尽量使用 make_shared()?

为了节省一次内存分配,原来shared_ptr<Foo> x(new Foo); 需要为 Fooref_count各分配一次内存,现在用make_shared()的话,可以一次分配一块足够大的内存,供Fooref_count对象容身。数据结构是:
在这里插入图片描述

常见问题分析

double free

在使用了智能指针之后,也是有可能出现double free的。

比如如下代码,它使用相同的内置指针初始化 p1 和 p2。在退出作用域后,会调用两次delete p,触发未定义行为,比如segment fault错误。

Widgt *raw_ptr = new Widgt;
std::shared_ptr<Widgt> p1(raw_ptr); // use_count = 1
std::shared_ptr<Widgt> p2(raw_ptr); // use_count = 1
// ...
// 会出现double free

因此,不要使用相同的内置指针初始化或 reset 多个 shared_ptr。

内存泄漏

主要是因为循环引用,导致对象不能被析构。具体见上文中的weak_ptr章节。

为什么多线程读写shared_ptr要加锁?

虽然对ref_count的读写是原子的,但是对shared_ptr中的ptrcnt的读写却不是原子的。在多线程下,如果存在多个线程读写同一个shared_ptr,就可能存在race condition

因此,如果存在多个线程读写同一个shared_ptr,最好使用互斥锁或者自旋锁来保证互斥。

参考

[1] 陈硕.Linux 多线程服务器端编程
[2] shared_ptr
[3] 陈硕.C++工程实践经验谈第2季
[4] C++ 智能指针浅析

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值