std::shared_ptr在多大程度上确保线程安全?

148

我正在阅读http://gcc.gnu.org/onlinedocs/libstdc++/manual/shared_ptr.html,但有些线程安全问题对我来说还不太清楚:

  1. 标准保证引用计数是线程安全的,并且与平台无关,对吗?
  2. 类似的问题 - 标准保证只有一个线程(持有最后一个引用)会调用共享对象的删除操作,对吗?
  3. shared_ptr不保证存储在其中的对象的任何线程安全性,对吗?

编辑:

伪代码:

// Thread I
shared_ptr<A> a (new A (1));

// Thread II
shared_ptr<A> b (a);

// Thread III
shared_ptr<A> c (a);

// Thread IV
shared_ptr<A> d (a);

d.reset (new A (10));

在线程IV中调用reset()会删除在第一个线程中创建的A类的先前实例,并用新实例替换它吗?此外,在IV线程中调用reset()后,其他线程只能看到新创建的对象吗?

29
你应该使用make_shared而不是new - qdii
4
回答所有问题(自编辑以来):对,对,对,错和错。 - John McFarlane
我已经尝试在这里给出了后端答案(https://dev59.com/x2Yq5IYBdhLWcg3wcgLq#65615682)。 - alex_noname
3个回答

113

正如其他人指出的那样,关于您最初的三个问题,您已经正确地解决了。

但是您编辑的结尾部分

在线程IV中调用reset()会删除在第一个线程中创建的A类的先前实例,并将其替换为新实例吗?此外,在IV线程中调用reset()后,其他线程只会看到新创建的对象吗?

是不正确的。只有d将指向新的A(10),而abc将继续指向原始的A(1)。这可以在以下简短的示例中清楚地看到。

#include <memory>
#include <iostream>
using namespace std;

struct A
{
  int a;
  A(int a) : a(a) {}
};

int main(int argc, char **argv)
{
  shared_ptr<A> a(new A(1));
  shared_ptr<A> b(a), c(a), d(a);

  cout << "a: " << a->a << "\tb: " << b->a
     << "\tc: " << c->a << "\td: " << d->a << endl;

  d.reset(new A(10));

  cout << "a: " << a->a << "\tb: " << b->a
     << "\tc: " << c->a << "\td: " << d->a << endl;
                                                                                                                 
  return 0;                                                                                                          
}

显然,我没有考虑任何线程:这不影响shared_ptr::reset()的行为。

此代码的输出为

a: 1 b: 1 c: 1 d: 1

a: 1 b: 1 c: 1 d: 10


49
  1. 正确的是,shared_ptr使用原子增减引用计数器来进行操作。

  2. 标准仅保证只有一个线程会调用共享对象的删除运算符。我不确定它是否具体指定了最后一个删除其共享指针副本的线程将会调用删除运算符(在实践中可能会是这种情况)。

  3. 不,它们不能,其中存储的对象可以被多个线程同时编辑。

编辑:稍微补充一下,如果你想了解共享指针的工作原理,你可能需要查看 boost::shared_ptr 的源代码:https://www.boost.org/doc/libs/release/boost/smart_ptr/shared_ptr.hpp


3
你提到的 "'shared_ptrs' 使用原子方式增加/减少引用计数值",意思是它们不使用任何内部锁来进行原子增加/减少操作,这会导致上下文切换。简单来说,多个线程可以在不使用锁的情况下增加/减少引用计数吗?而原子增量是通过特殊的 atomic_test_and_swap / atomic_test_and_increment 指令完成的? - rahul.deshmukhpatil
@rahul 编译器可以自由使用互斥锁,但大多数好的编译器在可以无锁完成的平台上不会使用互斥锁。 - Bernard
@Bernard:您的意思是它取决于平台上“编译器std lib shared_ptr”的实现吗? - rahul.deshmukhpatil
3
是的。据我所知,该标准并没有规定必须是无锁的。但在最新的GCC和MSVC中,在Intel x86硬件上它是无锁的,我认为其他好的编译器在硬件支持时也可能会这样做。 - Bernard

36

std::shared_ptr不是线程安全的。

共享指针是一对指针,一个指向对象,一个指向控制块(保存引用计数、弱指针链接等)。

可以有多个std::shared_ptr,每当它们访问控制块以更改引用计数时,都是线程安全的,但std::shared_ptr本身不是线程安全或原子安全的。

如果在另一个线程使用std::shared_ptr时将新对象分配给它,它可能会得到新对象指针,但仍然使用旧对象的控制块指针=> CRASH。


14
可以说单个std::shared_ptr实例不是线程安全的。根据std::shared_ptr参考文献,如果多个执行线程在没有同步的情况下访问同一个shared_ptr,并且其中任何一个访问使用了shared_ptr的非const成员函数,则会发生数据竞争。 - JKovalsky
17
这段话可以表述得更好。当使用“按值”(复制/移动)跨线程边界始终使用 std::shared_ptr<T> 实例时,它是保证线程安全的。而所有其他使用,如 std::shared_ptr<T>&,则在跨线程边界时不安全。 - WhiZTiM
我不在意你怎么命名它。我在这里提供的主要信息是其他答案中缺失的,即 std::shared_ptr 有多个成员。当了解到这一点后,所有进行多线程编程的人都能完全理解。使用不同的名称会使其更加模糊。 - Lothar
7
这个答案非常误导,而且不够清晰。是的,一个std::shared_ptr实例不是线程安全的,但这不是预期的使用情况,也不是问题所要求的。 - Martin Ba
3
这不是所询问的内容,因为原帖并未在线程之间共享share_ptr对象,除了初始化之外。每个线程都有自己的shared_ptr对象,并且据我所知,所有问题都涉及这些每个线程的shared_ptr对象(指向一个共享对象)。 - Martin Ba
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接