GCC 4.8.4 weak_ptr::lock() 的實作

fcamel
fcamel的程式開發心得
9 min readNov 9, 2016

基於效率考量,C++ 的 shared_ptr 和 weak_ptr 沒用 lock 保護。shared_ptr 的實作容易理解:

  • reference count 用 atomic +1/-1。
  • reference count 從 1 變 0 時,要刪除物件。這時只有一個 shared_ptr 指向物件,因此可以直接刪除,不用擔心 data race。

但是 weak_ptr 取出 shared_ptr 的時候也要查 reference count,而且 reference count 可以是任何數字,也可能剛好在 1 變 0 的時候取 shared_ptr。weak_ptr 如何保證 thread safety?

weak_ptr 取出 shared_ptr 的 API 是 weak_ptr::lock()。CPP Reference 這麼說:

Effectively returns expired() ? shared_ptr<T>() : shared_ptr<T>(*this), executed atomically.

所以 lock() 是 atomic 操作,不用擔心有 data race。好奇之下,看了 GCC 4.8.4 的實作。下圖是 shared_ptr 和 weak_ptr 的類別關係圖:

考量到 shared_ptr 都不存在時,可能還有 weak_ptr 要查看 reference count,所以得用另外的物件管理 reference count,不能直接放在 shared_ptr 裡。

從上圖可得知,weak_ptr 和 shared_ptr 主要的內容存在 super class __weak_ptr 和 __shared_ptr 身上。然後它們共享要管理的目標物件 (型別 _Tp),並透過 __weak_count/__shared_count 間接管理 reference count (型別 _Sp_counted_base)。

這幾個 class 的名稱和 member field 頗難記的,後面的程式碼可以和上圖對照,方便理解。

weak_ptr::lock() 程式如下:

// libstdc++-v3/include/bits/shared_ptr.h
shared_ptr<_Tp>
lock() const noexcept
{ return shared_ptr<_Tp>(*this, std::nothrow); }

就直接建一個 shared_ptr 傳出去。

shared_ptr() 的內容只有呼叫 __shared_ptr(),__shared_ptr() 程式如下:

// libstdc++-v3/include/bits/shared_ptr_base.h
__shared_ptr(const __weak_ptr<_Tp, _Lp>& __r, std::nothrow_t)
: _M_refcount(__r._M_refcount, std::nothrow)
{
_M_ptr = _M_refcount._M_get_use_count() ? __r._M_ptr : nullptr;
}

如果取得的 reference count > 0 就保留 __weak_ptr 託管的物件;反之則設為 nullptr,所以如何作到 atomic 的關鍵在生成 _M_refcount (型別 __shared_count):

template<_Lock_policy _Lp>
inline
__shared_count<_Lp>::
__shared_count(const __weak_count<_Lp>& __r, std::nothrow_t)
: _M_pi(__r._M_pi)
{
if (_M_pi != nullptr)
if (!_M_pi->_M_add_ref_lock_nothrow())
_M_pi = nullptr;
}

呼叫 _M_pi->_M_add_ref_lock_nothrow() 試著幫 reference count +1,成功的話保留指向 __Sp_counted_base 物件的指標;反之則否。

_M_add_ref_lock_nothrow() 的實作依 _Lock_policy 而有不同實作,分別如下:

template<>
inline bool
_Sp_counted_base<_S_single>::_M_add_ref_lock_nothrow()
{
if (_M_use_count == 0)
return false;
++_M_use_count;
return true;
}
template<>
inline bool
_Sp_counted_base<_S_mutex>::_M_add_ref_lock_nothrow()
{
__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;
}
template<>
inline bool
_Sp_counted_base<_S_atomic>::_M_add_ref_lock_nothrow()
{
// 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;
}

若 _Lock_policy 是 _S_atomic 的話 (推測有 87% 預設值是它),裡面就是使用 _M_get_use_count() 和__atomic_compare_exchange_n()。

__shared_ptr() 裡面也有呼叫 _M_get_use_count(),實作如下:

// libstdc++-v3/include/bits/shared_ptr_base.h
long _M_get_use_count() const noexcept
{ return _M_pi != 0 ? _M_pi->_M_get_use_count() : 0; }

所以重點落在 _Sp_counted_base::_M_get_use_count():

long _M_get_use_count() const noexcept
{
// No memory barrier is used here so there is no synchronization
// with other threads.
return __atomic_load_n(&_M_use_count, __ATOMIC_RELAXED);
}

只是一個 atomic load,而且是用 relaxed memory order,表示沒用到 memory barrier。這個操作不貴。關於 memory order 和 memory barrier,可以參考《簡介 C++11 atomic 和 memory order》

回頭看 __atomic_compare_exchange_n。依官方文件所述,成功的時候 memory order 會用 __ATOMIC_ACQ_REL,失敗的話用 __ATOMIC_RELAXED。所以成功的時候會用比較貴的 __ATOMIC_ACQ_REL (但只會作一次)。不懂為什麼這裡成功了要用 __ATOMIC_ACQ_REL,改天有閒再研究吧。所以 _Sp_counted_base<_S_atomic>::_M_add_ref_lock_nothrow() 就是不斷地檢查 reference count 的值,如果 reference count 為 0 就放棄。反之,試著 +1,若被其它 thread 先改值,就重讀值再試。在 reader 多 writer 少的情況,這樣比用 lock 有效率許多。

順道一提,_Lock_policy 的定義如下:

namespace __gnu_cxx _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
// Available locking policies:
// _S_single single-threaded code that doesn’t need to be locked.
// _S_mutex multi-threaded code that requires additional support
// from gthr.h or abstraction layers in concurrence.h.
// _S_atomic multi-threaded code using atomic operations.
enum _Lock_policy { _S_single, _S_mutex, _S_atomic };
// Compile time constant that indicates prefered locking policy in
// the current configuration.
static const _Lock_policy __default_lock_policy =
#ifdef __GTHREADS
#if (defined(__GCC_HAVE_SYNC_COMPARE_AND_SWAP_2) \
&& defined(__GCC_HAVE_SYNC_COMPARE_AND_SWAP_4))
_S_atomic;
#else
_S_mutex;
#endif
#else
_S_single;
#endif

我猜 x86 和 ARM 應該是 _S_atomic 吧?改天再確認這點。

到此真相大白,weak_ptr::lock() 的確很有效率,就只是生成一個 shared_ptr,如果目標物件還能用的話,會用一次 __ATOMIC_ACQ_REL 產生 memory barrier。目標物件不能用的話,不會用 memory barrier。

附帶一提,看了 GCC 實作的 weak_ptr 後,覺得 Chromium 的實作的確簡單太多了,效率更好,代價是禁止跨 thread 同時使用,但以 Chromium 的架構來說,不用擔心這點。

--

--