std :: shared_ptr线程安全

时间:2013-01-23 15:09:04

标签: c++ std shared-ptr

我读过那个

  

“多个线程可以同时读写不同   shared_ptr对象,即使对象是共享的副本   所有权。“(MSDN: Thread Safety in the Standard C++ Library

这是否意味着更改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的新值,但无论哪种方式,它都将具有对myClass的有效shared_ptr?< / p>

== EDIT ==
只是为了解释我的动机。我希望有一个共享指针来保存我的配置,我有一个线程池来处理请求 所以global是全局配置 thread 1正在开始处理请求时采用当前配置 thread 2正在更新配置。 (仅适用于未来的请求)

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

6 个答案:

答案 0 :(得分:80)

您所阅读的内容并不意味着您的意思。首先,尝试shared_ptr本身的msdn页面。

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

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

虽然通过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生活的地方,而不仅仅是从中读取。因此,从多个线程写入对象是不好的,除非你通过锁保护它。因此,您可以通过从对象中分配新的global_instance对象来读取shared_ptr<>对象,但您无法写入该对象。

// 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<>确保指向同一内存的多个副本没有本身的同步问题,但没有做任何事情对于指向的对象。这样对待它。

答案 1 :(得分:22)

为了补充Kevin写的内容,C ++ 14规范还支持对shared_ptr对象本身的原子访问:

  

20.8.2.6 shared_ptr原子访问[util.smartptr.shared.atomic]

     

如果访问是通过本节中的函数完成的,并且实例作为第一个参数传递,则从多个线程并发访问shared_ptr对象不会引入数据争用。

所以,如果你这样做:

//In thread 1
shared_ptr<myClass> private = atomic_load(&global);
...

//In thread 2
atomic_store(&global, make_shared<myClass>());
...

它将是线程安全的。

答案 2 :(得分:2)

读取操作之间不受数据竞争的影响,因此只要所有线程仅使用 const方法(包括创建副本),在线程之间共享同一个shared_ptr实例是安全的。它)。只要一个线程使用非const方法(如&#34;指向另一个对象&#34;),这样的使用就不再是线程安全的。

OP示例不是线程安全的,需要在线程1中使用原子加载,并在线程2中使用原子存储(C ++ 11中的2.7.2.5节)以使其线程安全。

MSDN文本中的关键词确实是不同的shared_ptr对象,如前面的答案所述。

答案 3 :(得分:1)

我认为,到目前为止,该问题的答案就所描述的情况而言具有误导性。我在问题中描述了一个非常相似的场景。所有其他线程仅(需要)通过以下方式对当前配置进行只读访问:

// In thread n
shared_ptr<MyConfig> sp_local = sp_global;

这些线程都不打算修改MyConfig对象的内容。上一行的每次执行,sp_global的引用计数都会增加。

线程1,定期将sp_global重置为配置的另一个实例:

// In thread 1
shared_ptr<MyConfig> sp_global = make_shared<MyConfig>(new MyConfig);

这也应该是安全的。它将sp_global的引用计数重新设置为1,sp_global现在指向最新配置​​,所有新的本地副本也是如此。因此,如果我在这里不丢失任何内容,那么这应该完全是线程安全的。

#include <iostream>
#include <memory>

using namespace std;

shared_ptr<int> sp1(new int(10));

int main()
{
    cout<<"Hello World! \n";

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "---------\n";

    shared_ptr<int> sp2 = sp1;
    shared_ptr<int>* psp3 = new shared_ptr<int>;
    *psp3 = sp1;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    sp1.reset(new int(20));

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    delete psp3;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "---------\n";

    sp1 = nullptr;

    cout << "sp1 use count: " << sp1.use_count() << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";

    return 0;
}

和输出

Hello World!
sp1 use count: 1, sp1: 10
---------
sp1 use count: 3, sp1: 10
sp2 use count: 3, sp2: 10
sp3 use count: 3, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 2, sp2: 10
sp3 use count: 2, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 1, sp2: 10
---------
sp1 use count: 0
sp2 use count: 1, sp2: 10

答案 4 :(得分:0)

这是我对shared_ptr的线程安全性的理解。 IMO,涉及shared_ptr的线程安全性有三个方面。

第一个是shared_ptr本身。我会说shared_ptr本身不是线程安全的,这意味着当我们尝试在多个线程中访问一个 shared_ptr对象并且其中一个正在写入时,会发生数据争用。例如,在以下情况下,我们将进行数据竞赛:

plotnames

第二个方面是shared_ptr的内部结构。我会说这是线程安全的。结果是,访问多个 shared_ptr对象时,没有数据争用,并且这些对象指向同一托管对象。例如,在以下情况下,我们不会进行数据争夺:

plotlist = list()
for (i in 1:3){
    plotlist[[plotnames[i]]] <- ggplot(mydata, aes(bygroup, fill = mydata[,vars[i]])) + 
      geom_bar(position = "fill") + 
      labs(title= vars[i], x="bygroup", y="") + 
    scale_fill_discrete(name=vars[i])
}

第三个方面是shared_ptr中的托管对象可能是线程安全的,也可能不是线程安全的。例如,我会在以下情况下发生数据争夺:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
global_ptr.reset();
参考文献

https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html#shared_ptr.thread

https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

答案 5 :(得分:0)

总结

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

  • <强>同样 std::shared_ptr实例可以由多个线程同时读取。

  • <强>同样 std::shared_ptr实例不能直接由多个线程而无需额外的同步修改。但可以通过互斥和原子学的手段来完成。


基本线程安全

该标准不说的智能指针的线程安全的东西,特别是std::shared_ptr,或他们是如何确保它。如@Kevin安德森如上所述,std::shared_ptr实际上提供用于共享对象的所有权,并确保其被正确地破坏,而不是提供正确的并发访问的设施。事实上,std::shared_ptr,像任何其他内置类型,都受到了所谓的basic thread-safety guarantee。这是在定义this纸为:

<块引用>

在基本线程安全保证将是标准库函数需要是可重入的,和非突变的标准库类型的对象的用途都要求没有引入数据争用。这很少或不影响性能。它确实提供了承诺的安全性。因此,这个基本的线程安全保证需要实现的。

作为用于标准,有以下措词:

[16.4.6.10/3]

<块引用>

一个C ++标准库函数应不直接或间接地修改由比当前线程其他线程访问的对象,除非对象通过该函数的非const参数,包括直接或间接地访问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.10/7]

<块引用>

实施方式可以在线程之间共享他们自己的内部对象如果对象对用户不可见的,并且针对数据竞争保护。

而这正是香草萨特calls 内部同步

<块引用>

那么,有什么的内部同步的目的是什么?这是唯一做到这一点的内部知道是共享的部分必要的同步,而且内部拥有,但主叫方不能同步,因为他不知道的共享和因为调用者不应该不需要拥有它们的内部做。因此在内部实行的类型,我们做的只是足够的内部同步的要回到那里调用者可以假设他平时常规意义上的关心和责任程度正确同步的任何对象实际上可能被共享。

因此,基本线程安全确保上的不同实例线程安全所有操作(包括拷贝构造拷贝赋值){ {1}},而不即使这些实例是副本,并且在同一对象的股权额外的同步。

斯特朗线程安全

但考虑以下情况:

std::shared_ptr

lambda 函数通过引用绑定 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(); } std::shared_ptr。因此,分配是在资源的争用条件和程序有未定义的行为。在基本线程安全保障的并不在这里工作,我们必须使用的强大的线程安全保障的。借此definition

<块引用>

强线程安全保障将是突变标准库类型的对象的用途都要求没有引入数据争用。这会对性能产生严重的负面影响。此外,真正的安全往往需要跨越几个成员函数调用锁定,每个函数调用锁定,从而提供将创造安全,其实不存在没有的错觉。由于这些原因,没有提供用于改变共享对象的全面强线程安全保证,并相应地对程序施加约束。

基本上,我们必须同步访问相同的ptr例如用于非const操作。我们可以通过以下方式做到这一点:

一些例子:

std::mutex

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