I'm reading the implements of std::shared_ptr of libstdc , and I noticed that libstdc has three locking policies: _S_single, _S_mutex, and _S_atomic (see here), and the lock policy would affect the specialization of class _Sp_counted_base (_M_add_ref and _M_release)
Below is the code snippet:
_M_release_last_use() noexcept
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
_M_dispose();
// There must be a memory barrier between dispose() and destroy()
// to ensure that the effects of dispose() are observed in the
// thread that runs destroy().
// See http://gcc.gnu.org/ml/libstdc /2005-11/msg00136.html
if (_Mutex_base<_Lp>::_S_need_barriers)
{
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
// Be race-detector-friendly. For more info see bits/c config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
-1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
template<>
inline bool
_Sp_counted_base<_S_mutex>::
_M_add_ref_lock_nothrow() noexcept
{
__gnu_cxx::__scoped_lock sentry(*this);
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, 1) == 0)
{
_M_use_count = 0;
return false;
}
return true;
}
my questions are:
- When using the _S_mutex locking policy, the __exchange_and_add_dispatch function may only guarantee atomicity, but may not guarantee that it is fully fenced, am I right?
- and because of 1, is the purpose of '__atomic_thread_fence (__ATOMIC_ACQ_REL)' to ensure that when thread A invoking _M_dispose, no thread will invoke _M_destory (so that thread A could never access a destroyed member(eg: _M_ptr) inside the function '_M_dispose'?
- What puzzles me most is that if 1 and 2 are both correct, then why there is no need to add a thread fence before invoking the '_M_dispose'? (because the objects managed by _Sp_counted_base and _Sp_counted_base itself have the same problem when the reference count is dropped to zero)
CodePudding user response:
Some of this is answered in the documentation and at the URL shown in the code you quoted.
- When using the _S_mutex locking policy, the __exchange_and_add_dispatch function may only guarantee atomicity, but may not guarantee that it is fully fenced, am I right?
Yes.
- and because of 1, is the purpose of '__atomic_thread_fence (__ATOMIC_ACQ_REL)' to ensure that when thread A invoking _M_dispose, no thread will invoke _M_destory (so that thread A could never access a destroyed member(eg: _M_ptr) inside the function '_M_dispose'?
No, that can't happen. When _M_release_last_use()
starts executing _M_weak_count >= 1
, and _M_destroy()
function won't be called until after _M_weak_count
is decremented, and so _M_ptr
remains valid during the _M_dispose()
call. If _M_weak_count==2
and another thread is decrementing it at the same time as _M_release_last_use()
runs, then there is a race to see which thread decrements first, and so you don't know which thread will run _M_destroy()
. But whichever thread it is, _M_dispose()
already finished before _M_release_last_use()
does its decrement.
The reason for the memory barrier is that _M_dispose()
invokes the deleter, which runs user-defined code. That code could have arbitrary side effects, touching other objects in the program. The _M_destroy()
function uses operator delete
or an allocator to release the memory, which could also have arbitrary side effects (if the program has replaced operator delete
, or the shared_ptr
was constructed with a custom allocator). The memory barrier ensures that the effects of the deleter are synchronized with the effects of the deallocation. The library doesn't know what those effects are, but it doesn't matter, the code still ensures they synchronize.
- What puzzles me most is that if 1 and 2 are both correct,
They are not.