C ++-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 global的原始值是thread 1还是线程2分配的新值,但是无论哪种方式,它都将对myClass有效的shared_ptr?

==编辑==
只是为了解释我的动机。 我想要一个共享的指针来保存我的配置,并且我有一个线程池来处理请求。
因此global是全局配置。
thread 1在开始处理请求时采用了当前配置。
thread 2正在更新配置。 (仅适用于将来的请求)

如果可行,我可以用这种方式更新配置,而不必在请求处理过程中破坏配置。

Roee Gavirel asked 2020-07-26T23:24:36Z
6个解决方案
86 votes

您正在阅读的内容并不意味着您认为的意思。 首先,请尝试针对shared_ptr本身的msdn页面。

向下滚动到“备注”部分,您将了解问题的实质。 基本上,shared_ptr<>指向“控制块”,这就是它跟踪多少shared_ptr<>对象实际指向“真实”对象的方式。 因此,当您执行此操作时:

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

虽然只有2个调用通过shared_ptr<>分配内存,但是有两个“逻辑”块不应该相同。 一个是存储实际值的shared_ptr<>,另一个是控制块,该控制块存储使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;

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

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

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

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

如果从任何其他线程(您说自己在做)访问shared_ptr<>,这几乎肯定不是很好。 如果要执行此操作,则需要锁定,因为您要写到shared_ptr<>所在的任何地方,而不仅仅是从它读取。 因此,从多个线程写入对象是不好的,除非它是通过锁保护的。 因此,您可以通过为其分配新的shared_ptr<>对象来从shared_ptr<>中读取该对象,但是您无法对其进行写入。

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

// In thread 4
*global_instance = 7;

shared_ptr<>的值不确定。 可能是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 answered 2020-07-26T23:25:37Z
24 votes

为了补充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>());
...

这将是线程安全的。

Chris Dodd answered 2020-07-26T23:26:15Z
4 votes

这意味着您将具有有效的2984134724588988930048和有效的引用计数。

您正在描述两个试图读取/分配给同一变量的线程之间的竞争条件。

因为这通常是未定义的行为(仅在单个程序的上下文和时间中才有意义),所以shared_ptr不能处理该行为。

Yochai Timmer answered 2020-07-26T23:26:44Z
2 votes

读操作彼此之间不受数据争用的约束,因此只要所有线程仅使用const方法(包括创建其副本),就可以在线程之间共享同一shared_ptr实例。 一旦一个线程使用非const方法(如“将其指向另一个对象”),这种使用就不再是线程安全的。

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

正如前面的答案中已经提到的那样,MSDN文本中的关键字确实是不同的shared_ptr对象。

Leon answered 2020-07-26T23:27:13Z
1 votes

我认为,到目前为止,该问题的答案在所描述的场景方面具有误导性。 我在问题中描述了一种非常相似的情况。 所有其他线程(仅)具有对当前配置的只读访问权限,可通过以下方式实现:

// 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
hagh answered 2020-07-26T23:27:51Z
0 votes

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

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

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

# Thread 1
global_ptr.reset();

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

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

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
local_ptr.reset();

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

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

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
(*local_ptr).clear();
参考资料

[HTTPS://GCC.弓弩.org/online doc S/里不是他的错++/manual/memory.HTML#shared_普通人.thread]

[HTTPS://恩.CPP reference.com/我/CPP/memory/shared_普通人/atomic]

Lujun Weng answered 2020-07-26T23:28:39Z
translate from https://stackoverflow.com:/questions/14482830/stdshared-ptr-thread-safety