何时应在多线程中使用带有 std::shared_ptr C++互斥锁?

When should I use a mutex with std::shared_ptr in C++ multithreading?

提问人:Sami 提问时间:6/30/2023 最后编辑:273KSami 更新时间:6/30/2023 访问量:205

问:

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 被多个线程修改。在第二个线程中,每个线程仅读取和复制共享指针。我是在两种情况下都需要互斥锁,还是只在第一种情况下需要互斥锁?为什么或者为什么不?

C++ 多线程 shared-ptr

评论

1赞 Jeremy Friesner 6/30/2023
呃,哦,你快要问C++ :)所有问题中最糟糕的问题了youtu.be/lkgszkPnV8g?t=1213
1赞 Solomon Slow 6/30/2023
TLDR:没有。变量并不比任何其他类型的变量更安全。如果多个线程访问(读取或写入)该变量,并且其中至少有一个线程写入该变量,则所有这些访问都必须“同步”。同步的一种方法是让所有变量在访问变量时都锁定相同的互斥锁。std::shared_ptr

答:

9赞 Sam Varshavchik 6/30/2023 #1

如果您仔细识别所涉及的所有活动部分,则更容易理解这一点:

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

  • 引用计数器本身,用于跟踪对引用计数对象的引用数

  • 使用引用计数器的共享指针本身

这些都是离散的实体,需要分别考虑和评估。

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

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

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

这将修改多个执行线程中的共享指针 。这需要同步,这不是线程安全的。您需要一个互斥锁。pd

std::shared_ptr<Dog> localCopy = pd;

这会复制 ,它不会修改它。这也兼顾了参考计数器,作为制作副本(和销毁)它的一部分。引用计数器是线程安全的。共享指针不会被修改,只会被访问。这是线程安全的。pd

评论

2赞 Jan Schultke 6/30/2023
您根本不需要与之交互。有.只有在您同时修改 时,互斥锁才变得必要。std::mutexstd::shared_ptrstd::atomic<std::shared_ptr>Dog
1赞 Solomon Slow 6/30/2023
@JanSchultke,在您自己的回答中,您承认自 C++20 起才可用。可能有一些项目尚未从较旧的 C++ 版本升级。std::atomic<std::shared_ptr>
0赞 Jan Schultke 6/30/2023
@SolomonSlow除非你使用的是 C++98(在这种情况下甚至没有 ),否则除了专用化之外,你还有可用的独立原子函数。那么真的没有充分的理由使用。std::shared_ptrstd::mutex
1赞 Solomon Slow 6/30/2023
@JanSchultke,IMO,没有充分的理由鼓励初级开发人员使用“原子”任何东西。您可以使用互斥锁来解决大多数与原子相同的问题,也可以使用互斥锁来解决许多其他原子无法解决的问题。真正需要原子的唯一地方是初级开发人员不应该接触的代码。理解原子的行为就像泥土一样简单,但了解如何有效地使用这种行为需要一种全新的思维方式。我说,“让新手先成为互斥锁的主人!
1赞 Sam Varshavchik 6/30/2023
这是正确的。
1赞 Jan Schultke 6/30/2023 #2

A 由三个部分组成:std::shared_ptr<Dog>

  1. 智能指针本身std::shared_ptr
  2. 指向原子参考计数器
  3. 指向对象Dog

您需要的同步形式取决于要同时修改的同步形式。

1 同时修改智能指针

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

这不是线程安全的,因为多个线程同时修改相同的 .基本上有三个选项:std::shared_ptr

理想情况下,使用专门为 . 相比之下,效率会较低,使用起来也不太方便。std::shared_ptrstd::mutex

2 同时修改参考计数器

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

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

3 同时修改指向对象

// for example
localCopy->woof();

如果你的成员函数不是线程安全的,你将需要使用一些同步来确保线程安全。 是一个很好的候选者,但您也可以考虑使用 .Dogwoofstd::mutexstd::atomic