std::shared_ptr 线程安全

新手上路,请多包涵

我读过

“多个线程可以同时读取和写入不同的 shared_ptr 对象,即使这些对象是共享所有权的副本。” ( MSDN:标准 C++ 库中的线程安全

这是否意味着更改 shared_ptr 对象是安全的?

例如,下一个代码是否被认为是安全的:

 shared_ptr<myClass> global = make_shared<myClass>();
...

//In thread 1
shared_ptr<myClass> private = global;
...

//In thread 2
global = make_shared<myClass>();
...

在这种情况下,我可以确定线程 1 private 将具有原始值 global 或线程 2 分配的新值,但无论哪种方式,它都会有一个有效的 shared_ptr 到 myClass?

==编辑==

只是为了解释我的动机。我想要一个共享指针来保存我的配置,并且我有一个线程池来处理请求。

所以 global 是全局配置。

thread 1 在开始处理请求时采用当前配置。

thread 2 正在更新配置。 (仅适用于未来的请求)

如果它有效,我可以以这种方式更新配置,而不会在请求处理过程中破坏它。

原文由 Roee Gavirel 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.1k
2 个回答

你所读的并不意味着你认为它意味着什么。首先,尝试 shared_ptr 本身的 msdn 页面。

向下滚动到“备注”部分,您将了解问题的实质。基本上,一个 shared_ptr<> 指向一个“控制块”,它是如何跟踪有多少 shared_ptr<> 对象实际上指向“真实”对象。所以当你这样做时:

 shared_ptr<int> ptr1 = make_shared<int>();

虽然这里只有 1 次调用通过 make_shared 分配内存,但有两个“逻辑”块您不应该同样对待。一个是 int 存储实际值,另一个是控制块,它存储了所有 shared_ptr<> 使其工作的“魔法”。

只有控制块本身是线程安全的。

为了强调,我把它放在了自己的位置上。 shared_ptr内容 不是线程安全的,也不是写入同一个 shared_ptr 实例。这里有一些东西可以证明我的意思:

 // In main()
shared_ptr<myClass> global_instance = make_shared<myClass>();
// (launch all other threads AFTER global_instance is fully constructed)

//In thread 1
shared_ptr<myClass> local_instance = global_instance;

这很好,实际上您可以在所有线程中随心所欲地执行此操作。然后当 local_instance 被破坏(超出范围)时,它也是线程安全的。有人可以访问 global_instance 这不会有什么不同。您从 msdn 中提取的代码段基本上意味着“对控制块的访问是线程安全的”,因此可以根据需要在不同的线程上创建和销毁其他 shared_ptr<> 实例。

 //In thread 1
local_instance = make_shared<myClass>();

这可以。它 影响 global_instance 对象,但只是间接的。它指向的控制块将被递减,但以线程安全的方式完成。 local_instance 将不再指向与 global_instance 相同的对象(或控制块)。

 //In thread 2
global_instance = make_shared<myClass>();

如果 global_instance 从任何其他线程(你说你正在做的)访问,这几乎肯定是不好的。如果您这样做,它需要一个锁,因为您正在写入 global_instance 生活的任何地方,而不仅仅是从中读取。所以从多个线程写入一个对象是不好的,除非你已经通过锁保护它。因此,您可以通过分配新的 shared_ptr<> 对象来读取 global_instance 对象,但您不能写入它。

 // In thread 3
*global_instance = 3;
int a = *global_instance;

// In thread 4
*global_instance = 7;

a 的值未定义。它可能是 7,也可能是 3,或者也可能是其他任何东西。 shared_ptr<> 实例的线程安全仅适用于管理相互初始化的 shared_ptr<> 实例,而不是它们指向的实例。

为了强调我的意思,看看这个:

 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;
    }
}

shared_ptr<> 是确保多个对象 所有者 确保对象被破坏的机制,而不是确保多个 线程 可以正确访问对象的机制。您仍然需要一个单独的同步机制才能在多个线程中安全地使用它(例如 std::mutex )。

考虑它的最佳方式 IMO 是 shared_ptr<> 确保指向同一内存的多个副本 本身 没有同步问题,但对指向的对象没有任何作用。就这样对待。

原文由 Kevin Anderson 发布,翻译遵循 CC BY-SA 4.0 许可协议

概括

  • 不同 std::shared_ptr 实例可以被多个线程同时读取和修改,即使这些实例是同一个对象的副本并且共享所有权。

  • 同一个 std::shared_ptr 实例可以被多个线程同时读取。

  • 同一个 std::shared_ptr 实例不能被多个线程直接修改,无需额外同步。但可以通过互斥体和原子来完成。


基本线程安全

该标准没有说明智能指针的线程安全性,特别是 std::shared_ptr ,或者它们如何帮助确保它。正如@Kevin Anderson 上面指出的那样, std::shared_ptr 实际上提供了一种共享对象所有权并确保正确销毁它的工具,而不是提供正确的并发访问。事实上, std::shared_ptr 与任何其他内置类型一样,都受制于所谓的 basic thread-safety guarantee 。在 本文 中定义为:

基本的线程安全保证是要求标准库函数是可重入的,并且要求标准库类型对象的非变异使用不引入数据竞争。这对性能影响很小或没有影响。它确实提供了承诺的安全性。因此,实现需要这种基本的线程安全保证。

至于标准,有以下措辞:

[16.4.6.103]

C++ 标准库函数不得直接或间接修改当前线程以外的线程可访问的对象,除非通过函数的非常量参数直接或间接访问对象,包括 this

由此可见,下面的代码必须被认为是线程安全的:

 std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
    std::thread([ptr]{
    auto local_p = ptr;  # read from ptr
    //...
    }).detach();
}

但是我们知道 std::shared_ptr 是一个 引用计数 指针,当使用计数变为零时,指向的对象将被删除。 std::shared_ptr 的引用计数块是标准库的实现细节。尽管上面有持续的操作( _读取_),但实现需要修改计数器。这种情况描述如下:

[16.4.6.107]

如果对象对用户不可见并且受到保护以防止数据竞争,则实现可以在线程之间共享它们自己的内部对象。

这就是 Herb Sutter 所说 的 _内部同步_:

那么 内部同步 的目的是什么?只是对内部知道共享和内部拥有的部分进行必要的同步,但是调用者无法同步,因为他不知道共享并且不需要因为调用者不知道拥有它们,内部拥有。因此,在类型的内部实现中,我们进行了足够的 _内部同步_,以回到调用者可以承担其通常的注意义务并以通常的方式正确同步任何可能实际共享的对象的级别。

因此, 基本线程安全 确保线程安全所有操作(包括 复制构造函数 和 _复制赋值_)在 std::shared_ptr 的不同实例上无需额外同步,即使这些实例是副本并共享同一对象的所有权。

强大的线程安全性

但考虑以下情况:

 std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
    std::thread([&ptr]{
    ptr = std::make_shared<int>(200);
    //...
    }).detach();
}

lambda 函数通过引用绑定 std::shared_ptr ptr 。因此,分配是资源上的竞争条件( ptr 对象本身)并且程序具有 _未定义的行为_。 基本的线程安全保证 在这里不起作用,我们必须使用 _强大的线程安全保证_。采取这个 定义

强大的线程安全保证 是标准库类型对象的变异使用需要不引入数据竞争。这将对性能产生严重的负面影响。此外,真正的安全通常需要跨多个成员函数调用进行锁定,因此提供每个函数调用锁定会产生实际上不存在的安全假象。由于这些原因,没有为改变共享对象提供全面的 _强大线程安全保证_,并且相应地对程序施加了约束。

基本上,我们必须同步访问相同的 std::shared_ptr 实例以进行非常量操作。我们可以通过以下方式做到这一点:

一些例子:

std::mutex

 std::shared_ptr<int> ptr = std::make_shared<int>(100);
std::mutex mt;

for (auto i= 0; i<10; i++){
    std::thread([&ptr, &mt]{
      std::scoped_lock lock(mt);
      ptr = std::make_shared<int>(200);
      //...
      }).detach();
}

原子函数:

 std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
  std::thread([&ptr]{
    std::atomic_store(&ptr, std::make_shared<int>(200));
  }).detach();
}

原文由 alex_noname 发布,翻译遵循 CC BY-SA 4.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题