提问人:Bin Yan 提问时间:11/9/2022 最后编辑:EvgBin Yan 更新时间:11/9/2022 访问量:268
C++ std::shared_ptr 的递增和递减引用计数的竞赛 [duplicate]
C++ Race of incrementing and decrementing reference counting of std::shared_ptr [duplicate]
问:
从引用中,我知道它本身是线程安全的,因为引用计数通常由一些具有 .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>
答:
2赞
Wintermute
11/9/2022
#1
shared_ptr
在内部不使用 etc.,而是使用 .从 GNU 实现:fetch_add
compare_exchange
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
评论
0赞
François Andrieux
11/9/2022
shared_ptr
的分配不是原子的。原始代码中仍存在争用条件。检查是否有效是不够的。如果尚未进入UB中,则不可能出现问题中描述的场景,并表明存在潜在的混淆。ptr
ptr2
0赞
Wintermute
11/9/2022
是否确定?我以为有保证,只是没有它引用的对象。shared_ptr
3赞
François Andrieux
11/9/2022
只有控制块是同步的。这意味着,如果您有两个线程,每个线程都对同一对象有自己的线程并使用相同的引用计数器,则每个线程都可以安全地重新分配或重置自己的线程。但是,如果两个线程对同一指针执行相同的操作,则不允许它们避免数据争用。shared_ptr
shared_ptr
shared_ptr
1赞
François Andrieux
11/9/2022
请参阅 en.cppreference.com/w/cpp/memory/shared_ptr:所有成员函数(包括复制构造函数和复制赋值)都可以由不同shared_ptr实例上的多个线程调用,而无需额外的同步,即使这些实例是副本并共享同一对象的所有权。如果多个执行线程在没有同步的情况下访问同一个shared_ptr实例,并且其中任何一个访问使用非常量成员函数 shared_ptr,则将发生数据争用。
5赞
François Andrieux
11/9/2022
问题很简单,在一个线程和另一个线程中,没有同步并使用相同的对象是 UB。 没有提供例外。ptr = /*something*/;
/*something/* = ptr;
ptr
shared_ptr
评论
ptr
std::shared_ptr
T
shared_ptr<T>
shared_ptr<int>
shared_ptr
reference counting decrease here! Call ~T()
ptr
1
make_shared
std::shared_ptr
ptr
std::shared_ptr
std::thread t([&ptr] ()...
ptr
ptr