来自the reference我知道std::shared_ptr<T>
本身是线程安全的,因为引用计数通常由一些std::atomic
来实现与 std::memory_order_relaxed
.
但是我还是不知道怎么办std::shared_ptr
确保引用计数器并发递增和递减下的线程安全。
即,
Thread 1:
// destructing shared_ptr<T>
// rc.fetch_sub(1)
// Thread 1 got RC == 0 and *thought* that he is the last owner
// ** call T::~T()
Thread 2:
// concurrently copy that shared_ptr<T>
// rc.fetch_add(1)
// Thread 2 got RC == 1 and *thought* that the copied shared_ptr is valid
// dereference the shared_ptr
// ** accessing the destructed T !
这种情况虽然存在种族问题,但并非不可能。 下面是示例代码(手动构造一个极端情况)。
std::shared_ptr<T> ptr;
int main()
{
std::thread t([&ptr] ()
{
ptr = std::make_shared<int>();
} // reference counting decrease here! Call ~T()
);
auto ptr2 = ptr; // reference counting increase here!
ptr2->some_function(); // access destructed object!
t.join();
}
我的问题是:
- C++ 引用文献如何描述此案例?
- 是
std::shared_ptr<T>
即使在引用计数器并发递增和递减的情况下也能确保线程安全吗?
最佳答案
shared_ptr
内部不使用 fetch_add
等,而是使用 compare_exchange
。来自 GNU 实现:
template<>
inline bool
_Sp_counted_base<_S_atomic>::
_M_add_ref_lock_nothrow() noexcept
{
// Perform lock-free add-if-not-zero operation.
_Atomic_word __count = _M_get_use_count();
do
{
if (__count == 0)
return false;
// Replace the current counter value with the old value + 1, as
// long as it's not changed meanwhile.
}
while (!__atomic_compare_exchange_n(&_M_use_count, &__count, __count + 1,
true, __ATOMIC_ACQ_REL,
__ATOMIC_RELAXED));
return true;
}
有一些样板文件,但本质上它的作用是获取当前计数,检查它是否为零,否则在读取后该值没有更改的预期下执行原子compare_exchange操作。这是无锁(但不是无等待)数据结构中相当常见的机制。如果你稍微眯一下眼睛,你可以将其称为一种用户空间自旋锁。与互斥锁相比,这是有利的,因为它可以节省非常昂贵的系统调用,除非对shared_ptr的争用非常高。
顺便说一句,您的代码应该在使用前检查 ptr2
是否有效,因为线程可能在主线程中的复制之前结束。但这似乎与您的实际问题无关。
关于C++ std::shared_ptr 递增和递减引用计数的竞赛,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/74364000/