shared_ptr本身是安全无锁的,但shared_ptr对象的读写并不是,因为shared_ptr拥有两个数据成员,读写操作不能原子化。

参考文献:C++ shared_ptr四宗罪(不得不转)shared_ptr共享型智能指针详解

shared_ptr数据结构

shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。具体来说,shared_ptr 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。
多线程shared_ptr竞争问题 - 图1

  • 控制块中的数据有指向Foo的指针,为何?

shared_ptr()在构造对象时,捕获了对象的析构行为。即shared_ptr.ptrref_count.ptr可以是不同的类型(存在隐式转换)
1. 无需虚析构;假设 Bar 是 Foo 的基类,但是 Bar 和 Foo 都没有虚析构。

  1. shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*
  2. shared_ptr<Bar> sp2 = sp1; // 可以赋值,自动向上转型(up-cast)
  3. sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为其 ref_count 记住了 Foo 的实际类型。
2.** shared_ptr<void>** 可以指向并安全地管理(析构或防止析构)任何对象;muduo::net::Channel class 的 tie() 函数就使用了这一特性,防止对象过早析构,见书 7.15.3 节。

  1. shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*
  2. shared_ptr<void> sp2 = sp1; // 可以赋值,Foo* 向 void* 自动转型
  3. sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,不会出现 delete void 的情况,因为 delete 的是 ref_count.ptr,不是 sp2.ptr。
*3. 多继承。
假设 Bar 是 Foo 的多个基类之一,那么:

  1. shared_ptr<Foo> sp1(new Foo);
  2. shared_ptr<Bar> sp2 = sp1; // 这时 sp1.ptr 和 sp2.ptr 可能指向不同的地址,因为 Bar subobject 在 Foo object 中的 offset 可能不为0。
  3. sp1.reset(); // 此时 Foo 对象的引用计数降为 1

但是 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为 delete 的不是 Bar,而是原来的 Foo。换句话说,sp2.ptr 和 ref_count.ptr 可能具有不同的值(当然它们的类型也不同)。

多线程无保护写造成race condition

  1. shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
  2. shared_ptr<Foo> x; // 线程 A 的局部变量
  3. shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量
  4. //线程A执行
  5. x=g;
  6. //线程B执行
  7. g=n;
  8. //此时会造成竞争
  • 初始状态

多线程shared_ptr竞争问题 - 图2

  • x=g,仅仅先完成了指针的指向,控制块还未完成

多线程shared_ptr竞争问题 - 图3

  • g=n,2个步骤均先完成了,此时Foo1对象析构

多线程shared_ptr竞争问题 - 图4

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

为什么要尽量用make_shared——节省1次内存分配

在调用share_ptr()构造智能指针对象是,需要2次内存分配,若使用make_shared,不仅 只需要一次内存分配,且异常安全。
多线程shared_ptr竞争问题 - 图5