在C++多线程中,何时应该使用互斥锁(mutex)与std::shared_ptr一起使用?

6
std::shared_ptr<Dog> pd;

void F() {
    pd = std::make_shared<Dog>("Smokey");
}

int main() {
    std::thread t1(F);
    std::thread t2(F);
    t1.join();
    t2.join();
    return 0;
}

std::shared_ptr<Dog> pd(new Dog("Gunner"));

void F() {
    std::shared_ptr<Dog> localCopy = pd;
}

int main() {
    std::thread t1(F);
    std::thread t2(F);
    t1.join();
    t2.join();
    return 0;
}

在C++中,我了解到std::shared_ptr对于读取和复制是线程安全的。但是我对于何时需要使用互斥锁来同步线程有些困惑。我有两段代码片段。在第一段代码中,std::shared_ptr被多个线程修改。在第二段代码中,每个线程只是从共享指针中读取和复制。在这两种情况下,我是否都需要互斥锁?为什么或者为什么不需要?

1
哎呀,你快要问出C++中最糟糕的问题了 :) https://youtu.be/lkgszkPnV8g?t=1213 - Jeremy Friesner
1
哦哦,你正冒险接近提出C++中最糟糕的问题 :) https://youtu.be/lkgszkPnV8g?t=1213 - Jeremy Friesner
1
简而言之:不行。std::shared_ptr变量并不比其他类型的变量更具线程安全性。如果多个线程访问(读取或写入)该变量,并且其中至少一个线程对其进行写入操作,那么所有这些访问都必须进行“同步”。一种同步的方法是在访问该变量时,所有线程都锁定相同的互斥锁。 - Solomon Slow
1
总结:不是的。std::shared_ptr变量在线程安全方面并不比其他类型的变量更安全。如果多个线程访问(读取或写入)该变量,并且其中至少一个线程对变量进行写入操作,则_所有_这些访问都必须进行“同步”。一种同步的方法是,所有线程在访问该变量时都锁定相同的互斥量。 - Solomon Slow
2个回答

9

如果你仔细识别所有涉及的移动部件,这将更容易理解:

  • 引用计数对象,这是一个对象,在某个地方,这就是你的Dog

  • 引用计数本身,它跟踪对引用计数对象的引用数量

  • 智能指针本身,使用引用计数

这些都是需要单独考虑和评估的离散实体。

作为一个经验法则,在C++中,如果一个对象从多个执行线程访问,并且至少有一个执行线程以某种方式“修改”它,那么执行线程必须与该对象“同步”;除非该对象是“线程安全”的。什么是“同步”?嗯,它不仅仅意味着在某个地方有一个互斥锁;但对于这个实际例子来说,它的意思是:你需要在持有某个互斥锁的同时访问该对象。

数据点:引用计数是线程安全的。智能指针,即std::shared_ptr不是。

pd = std::make_shared<Dog>("Smokey");

这会在多个执行线程中修改共享指针pd。这需要同步,不是线程安全的。你需要一个互斥锁。
std::shared_ptr<Dog> localCopy = pd;

这将创建一个 pd 的副本,而不是修改它。这也会处理引用计数器,作为创建副本(和销毁)的一部分。引用计数器是线程安全的。共享指针没有被修改,只是被访问。这是线程安全的。

2
在与std::shared_ptr交互时,根本不需要使用std::mutex。有std::atomic<std::shared_ptr>可以使用。只有在同时修改Dog时,互斥锁才变得必要。 - Jan Schultke
2
你根本不需要使用std::mutex来与std::shared_ptr进行交互。有std::atomic<std::shared_ptr>可以使用。只有在同时修改Dog时,互斥锁才变得必要。 - Jan Schultke
1
@JanSchultke,在你自己的回答中,你承认std::atomic&lt;std::shared_ptr&gt;只在C++20之后才可用。可能存在一些尚未从较旧的C++版本升级的项目。 - Solomon Slow
1
这是正确的。 - Sam Varshavchik
1
这是正确的。 - Sam Varshavchik
显示剩余12条评论

1
一个`std::shared_ptr`由三个组件组成:
  1. 智能指针`std::shared_ptr`本身
  2. 指向的原子引用计数器
  3. 指向的`Dog`对象

你需要哪种形式的同步取决于你想要同时修改哪一个。

1 同时修改智能指针

// for example
pd = std::make_shared<Dog>("Smokey");

这是线程不安全的,因为多个线程同时修改同一个std::shared_ptr。基本上有三种选择: 理想情况下,使用专门为std::shared_ptr设计的工具之一。与之相比,std::mutex效率较低,并且使用起来也不太方便。

2 同时修改引用计数器

// for example
std::shared_ptr<Dog> localCopy = pd;

在这里你不需要使用std::mutex或其他任何东西,因为它已经是线程安全的。复制一个std::shared_ptr不会修改原始对象,并且它会线程安全地更新(原子)引用计数器。

3 同时修改指向的对象

// for example
localCopy->woof();

如果你的`Dog`类的`woof`成员函数不是线程安全的,你需要使用一些同步机制来确保线程安全。`std::mutex`是一个很好的选择,但你也可以考虑使用`std::atomic`。

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