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_ptr
的 class 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()
意外删除。此外,方法 method1
和 method2
是常量,因此它不会修改内部对象。但我可能是错的。
所以我有两个问题:
- 上面的代码是线程安全的吗?
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
,它被设计成一个简单的作用域锁。
假设我们有一个 class A
带有 const 方法(仅)。
class A
{
public:
void method1() const;
void method2() const;
}
此外,我们还有另一个 class B
,它具有 shared_ptr
的 class 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()
意外删除。此外,方法 method1
和 method2
是常量,因此它不会修改内部对象。但我可能是错的。
所以我有两个问题:
- 上面的代码是线程安全的吗?
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
,它被设计成一个简单的作用域锁。