shared_ptr 和 const 方法线程安全

shared_ptr and const methods thread-safety

假设我们有一个 class A 带有 const 方法(仅)。

class A
{
public:
    void method1() const;
    void method2() const;
}

此外,我们还有另一个 class B,它具有 shared_ptrclass A 以及 return 它的方法,并使用互斥保护对其进行更改:

class B
{
    std::shared_ptr<A> ptr;
    std::mutex m;
public:
    B()
    {
        ptr = std::make_shared<A>();
    }

    std::shared_ptr<A> getPtr() const
    {
        mutex_on_stack mut(m);
        return ptr;
    }

    void resetPtr()
    {
        mutex_on_stack mut(m);
        ptr.reset(new A());
    }
}

ptr 变量受互斥锁保护,所以我假设它是线程安全的。但是我不确定内部对象本身的安全性。

B objB;

//thread 1
auto p1 = objB->getPtr();
p1->method1(); //calling const method

//thread 2
auto p2 = objB->getPtr();
p2->method2(); //calling const method

//thread 3
objB->resetPtr();

线程 1 和 2 调用 getPtr() 方法并在复制时增加 shared_ptr 的引用计数器。所以我认为内部指针不会被线程 3 中的 resetPtr() 意外删除。此外,方法 method1method2 是常量,因此它不会修改内部对象。但我可能是错的。

所以我有两个问题:

  1. 上面的代码是线程安全的吗?
  2. class A 线程安全吗?

Class A 是线程安全的,因为它只使用 const 函数,可以假设这些函数不会改变外部可见状态。虽然 const 函数能够 修改 内部状态,但如果 class 也有 mutable 成员,则通常假定任何 mutable 成员是同步的,因此一个对象的两个并发读取器永远不会遇到竞争条件。


访问 shared_ptr 的内容是线程安全的,实际上甚至不需要额外的互斥锁同步。来自 cppreference:

All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

但是,这仅在您不调用相关 shared_ptr 的非 const 函数时才成立:

If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

所以如果没有互斥量,class B 现在就不是线程安全的,因为你在函数上调用 reset。如果您使用 atomic overloads for shared_ptr 而不是调用 reset,那么在没有互斥锁的情况下,它将成为线程安全的。

实际上,要确保线程安全,您唯一需要做的就是确保您的 RAII 包装器持续到它正在执行的函数作用域结束。

关于 RAII 包装器的主题,您不需要推出自己的锁定-解锁机制。标准库已经以 lock_guard, unique_lock and shared_lock 的形式为您提供了 RAII 锁。像这样的简单场景可能最适合 lock_guard,它被设计成一个简单的作用域锁。