- 前言
- 控制塊簡介
- 共享控制塊
- 引用計數與弱引用計數建立過程
- __shared_ptr
- __shared_count
- _Sp_counted_base
- 弱引用計數增加過程
- 再談共享控制塊
- __weak_count
- 引用計數增加過程
- 弱引用計數的減少過程
- 弱引用計數減為0
- 引用計數的減少過程
- 引用計數減為0
- 參考文章
前言
本文結合原始碼討論std::shared_ptr和std::weak_ptr的部分底層實現,然後討論引用計數,弱引用計數的建立和增減。
文章中儘可能的先闡述原理,然後再貼上程式碼。如果有不想看程式碼的,直接略過程式碼即可。
本文涉及的原始碼均出自gcc 9.4.0版本
控制塊簡介
控制塊是shared_ptr
和weak_ptr
中的重要組成,主要用於管理資源的引用計數和生命週期。這個機制允許智慧指標安全地共享和管理同一個物件,同時自動釋放不再需要的資源。
控制塊包含以下部分:
- 引用計數
- 弱引用計數
- 分配器
- 刪除器
本文討論的引用計數和弱引用計數的建立、加減、銷燬,與控制塊密切相關。
共享控制塊
首先我們要知道,當建立一個std::shared_ptr
指向某個物件時,會生成一個控制塊來儲存該物件的引用計數和其他管理資訊。如果基於這個std::shared_ptr
再建立一個或多個std::weak_ptr
,那麼這些std::weak_ptr
將也指向這個控制塊。
示意圖大概長這樣:
引用計數與弱引用計數建立過程
在談引用計數和弱引用計數的建立時,其實就是討論控制塊的建立。
我們知道std::weak_ptr
是被設計用來解決std::shared_ptr
智慧指標可能導致的迴圈引用問題。一個有效的std::weak_ptr
物件一般是透過std::shared_ptr
構造的或者是透過複製(移動)其他std::weak_ptr
物件得到的,std::weak_ptr
物件的構造不涉及控制塊的建立。
因此在討論引用計數、弱引用計數的建立時,我們是去分析std::shared_ptr
的原始碼
__shared_ptr
__shared_ptr
是std::shared_ptr
的核心實現,它位於shared_ptr_base.h
中。
__shared_ptr
在構造例項時都會構造一個_M_refcount
,它的型別為__shared_count<_Lp>
。
//file: shared_ptr_base.h
template<typename _Tp, _Lock_policy _Lp>
class __shared_ptr : public __shared_ptr_access<_Tp, _Lp>
{
public:
using element_type = typename remove_extent<_Tp>::type;
//預設構造
constexpr __shared_ptr() noexcept
: _M_ptr(0), _M_refcount()
{ }
...
//有刪除器和分配器的構造
template<typename _Yp, typename _Deleter, typename _Alloc,
typename = _SafeConv<_Yp>>
__shared_ptr(_Yp* __p, _Deleter __d, _Alloc __a)
: _M_ptr(__p), _M_refcount(__p, std::move(__d), std::move(__a))
{
static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
"deleter expression d(p) is well-formed");
_M_enable_shared_from_this_with(__p);
}
private:
...
element_type* _M_ptr; // Contained pointer.
__shared_count<_Lp> _M_refcount; // Reference counter.
};
__shared_count
在建立__shared_count
物件時,也會建立一個指向控制塊的指標(_Sp_counted_base
型別的指標)。控制塊用來管理引用計數。
程式碼中的_Sp_counted_ptr
和_Sp_counted_deleter
就是_Sp_counted_base
的派生類。
//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
//預設構造
__shared_count(_Ptr __p) : _M_pi(0)
{
__try
{
_M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
}
__catch(...)
{
delete __p;
__throw_exception_again;
}
}
//帶分配器和刪除器的構造
template<typename _Ptr, typename _Deleter, typename _Alloc,
typename = typename __not_alloc_shared_tag<_Deleter>::type>
__shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
{
typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
__try
{
typename _Sp_cd_type::__allocator_type __a2(__a);
auto __guard = std::__allocate_guarded(__a2);
_Sp_cd_type* __mem = __guard.get();
::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
_M_pi = __mem;
__guard = nullptr;
}
__catch(...)
{
__d(__p); // Call _Deleter on __p.
__throw_exception_again;
}
}
private:
friend class __weak_count<_Lp>;
_Sp_counted_base<_Lp>* _M_pi;
};
_Sp_counted_base
_Sp_counted_base
負責管理引用計數和弱引用計數,其中
_M_use_count
是shared_ptr
的計數,就是引用計數,表示有多少個shared_ptr
物件共享同一個記憶體資源。_M_weak_count
是weak_ptr
的計數,也就是弱引用計數,表示有多少個weak_ptr
物件引用同一個資源。
我們可以看到在_Sp_counted_base
的初始化列表中,初始化了_M_use_count
和_M_weak_count
為1,完成了引用計數和弱引用計數的建立和初始化。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
public:
_Sp_counted_base() noexcept : _M_use_count(1), _M_weak_count(1) { }
...
private:
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
};
這裡再簡單提一下_Sp_counted_base
、_Sp_counted_ptr
和_Sp_counted_deleter
的關係與各自的功能。
_Sp_counted_base
是一個抽象基類,定義並管理了引用計數與弱引用記數。_Sp_counted_ptr
繼承自_Sp_counted_base
,主要是使用預設的分配策略和刪除策略管理資源物件。_Sp_counted_deleter
繼承自_Sp_counted_base
,主要是使用使用者提供的分配器和刪除器管理資源物件。
因為_Sp_counted_base
是抽象基類無法被例項化,所以使用的是其派生類_Sp_counted_ptr
和_Sp_counted_deleter
物件來管理引用計數、弱引用計數、分配器、刪除器。這個物件就是我們常說的控制塊。
(_Sp_counted_base
還有一個派生類_Sp_counted_ptr_inplace
,適合使用std::make_shared
的場景,此處不過多討論)
弱引用計數增加過程
再談共享控制塊
在上面的引用計數與弱引用計數建立過程中,我們提到:
一個有效的
std::weak_ptr
物件一般是透過std::shared_ptr
構造的或者是透過複製(移動)其他std::weak_ptr
物件得到的
對應的__weak_count
和__shared_count
物件也具有上述關係。
檢視原始碼,我們可以發現,__weak_count
和__shared_count
都有一個指向控制塊的多型指標。
_Sp_counted_base<_Lp>* _M_pi;
在__weak_count
中並沒有使用new
或者類似操作讓_M_pi
指向一塊新的記憶體(控制塊)。追根溯源,__weak_count
中多型指標指向的控制塊的來源就是__shared_count
。程式碼中是透過在__weak_count
建構函式和過載的賦值運算子中給多型指標_M_pi
初始化和賦值實現的。以此實現了weak_ptr
和shared_ptr
共享控制塊的功能。
__weak_count
弱引用計數的增加可以分為下面幾種情況:
- 透過
std::shared_ptr
構造std::weak_ptr
- 透過
std::weak_ptr
構造std::weak_ptr
- 透過
std::shared_ptr
給std::weak_ptr
賦值 - 透過
std::weak_ptr
給std::weak_ptr
賦值
其實本質是靠呼叫_M_weak_add_ref()
增加的弱引用計數,詳情見__weak_count
的原始碼:
//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __weak_count
{
public:
...
//透過__shared_count構造
//和一個已存在的__shared_count物件共享控制塊,並更新控制塊的弱引用計數
__weak_count(const __shared_count<_Lp>& __r) noexcept
: _M_pi(__r._M_pi)
{
//若入參的多型指標不為空
//弱引用計數++(增加_Sp_counted_base物件的_M_weak_count)
if (_M_pi != nullptr)
_M_pi->_M_weak_add_ref();
}
//透過__weak_count複製構造
//和傳入的__weak_count物件就共享同一個控制塊,並更新控制塊的弱引用計數
__weak_count(const __weak_count& __r) noexcept
: _M_pi(__r._M_pi)
{
if (_M_pi != nullptr)
_M_pi->_M_weak_add_ref();
}
//透過__shared_count給__weak_count賦值
__weak_count& operator=(const __shared_count<_Lp>& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
//新物件弱引用計數++
if (__tmp != nullptr)
__tmp->_M_weak_add_ref();
//原物件弱引用計數--
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
//指向新物件的控制塊
_M_pi = __tmp;
return *this;
}
//透過__weak_count給__weak_count賦值
__weak_count& operator=(const __weak_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != nullptr)
__tmp->_M_weak_add_ref();
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
_M_pi = __tmp;
return *this;
}
...
private:
friend class __shared_count<_Lp>;
_Sp_counted_base<_Lp>* _M_pi;
};
引用計數增加過程
引用計數的增加可以分為下面幾種情況:
- 透過
std::shared_ptr
構造std::shared_ptr
- 透過
std::shared_ptr
給std::shared_ptr
賦值 std::weak_ptr
升級為std::shared_ptr
本質是靠呼叫_M_add_ref_copy()
和_M_add_ref_lock
增加的引用計數,詳情見__shared_count
的原始碼:
//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
//複製構造
__shared_count(const __shared_count& __r) noexcept
: _M_pi(__r._M_pi)
{
if (_M_pi != 0)
_M_pi->_M_add_ref_copy();
}
//複製賦值
__shared_count& operator=(const __shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != _M_pi)
{
if (__tmp != 0)
__tmp->_M_add_ref_copy();
if (_M_pi != 0)
_M_pi->_M_release();
_M_pi = __tmp;
}
return *this;
}
//轉換構造
//weak_ptr使用lock()時會呼叫此建構函式
explicit __shared_count(const __weak_count<_Lp>& __r)
: _M_pi(__r._M_pi)
{
if (_M_pi != nullptr)
_M_pi->_M_add_ref_lock();//引用計數++,具體實現依賴於鎖策略
else
__throw_bad_weak_ptr();
}
private:
friend class __weak_count<_Lp>;
_Sp_counted_base<_Lp>* _M_pi;
};
弱引用計數的減少過程
弱引用計數的減少可以分為下面幾種情況:
std::weak_ptr
析構std::weak_ptr
物件被覆蓋(賦值操作覆蓋原std::weak_ptr
)
本質是靠呼叫_M_weak_release()
減少弱引用計數:
//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __weak_count
{
public:
//析構
~__weak_count() noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
}
//轉換賦值
__weak_count& operator=(const __shared_count<_Lp>& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != nullptr)
__tmp->_M_weak_add_ref();
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
_M_pi = __tmp;
return *this;
}
//複製賦值
__weak_count& operator=(const __weak_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != nullptr)
__tmp->_M_weak_add_ref();
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
_M_pi = __tmp;
return *this;
}
//移動賦值
__weak_count& operator=(__weak_count&& __r) noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_weak_release();
_M_pi = __r._M_pi;
__r._M_pi = nullptr;
return *this;
}
private:
friend class __shared_count<_Lp>;
_Sp_counted_base<_Lp>* _M_pi;
};
然後在這裡對std::weak_ptr::reset()
說明一下:它是用來重置 std::weak_ptr
的。呼叫 reset()
會使std::weak_ptr
不再指向它原本觀察的物件。
它也會減少原物件的弱引用計數(本質是透過呼叫的解構函式使得弱引用計數減少)
//file: shared_ptr_base.h
void reset() noexcept
{
__weak_ptr().swap(*this);
}
弱引用計數減為0
在上面提到:弱引用計數的減少是透過呼叫_M_weak_release()
實現的。透過分析_M_weak_release()
的程式碼我們可以知道,_M_weak_release()
中主要做了:
- 對弱引用計數做減1操作並
- 判斷弱引用計數減1後是否為0,若為0則呼叫
_M_destroy()
刪除控制塊。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
//控制塊的弱引用計數為0時,銷燬自身
virtual void _M_destroy() noexcept
{ delete this; }
//弱引用計數--
//當弱引用計數變為0,銷燬控制塊
void _M_weak_release() noexcept
{
// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
//減少弱引用計數,並返回-1之前的值
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
if (_Mutex_base<_Lp>::_S_need_barriers)
{
// See _M_release(),
// destroy() must observe results of dispose()
__atomic_thread_fence (__ATOMIC_ACQ_REL);
}
_M_destroy();
}
}
};
引用計數的減少過程
引用計數的減少可以分為下面幾種情況:
std::shared_ptr
析構std::shared_ptr
物件被覆蓋(賦值操作覆蓋原std::shared_ptr
)
本質是靠呼叫_M_release()
減少弱引用計數
//file: shared_ptr_base.h
template<_Lock_policy _Lp>
class __shared_count
{
public:
//析構
~__shared_count() noexcept
{
if (_M_pi != nullptr)
_M_pi->_M_release();
}
//複製賦值
__shared_count& operator=(const __shared_count& __r) noexcept
{
_Sp_counted_base<_Lp>* __tmp = __r._M_pi;
if (__tmp != _M_pi)
{
if (__tmp != 0)
__tmp->_M_add_ref_copy();
if (_M_pi != 0)
_M_pi->_M_release();
_M_pi = __tmp;
}
return *this;
}
private:
friend class __weak_count<_Lp>;
_Sp_counted_base<_Lp>* _M_pi;
};
引用計數減為0
上面提到:引用計數的減少是透過呼叫_M_release()
實現的。透過分析_M_release()
的程式碼我們可以知道,_M_release()
中主要做了
- 對引用計數做減1操作並
- 判斷引用計數減1後是否為0,若為0則呼叫
_M_dispose()
釋放其所管理的記憶體資源 - 若引用計數減1後為0,則還會對弱引用計數做一次減1操作並
- 判斷弱引用計數減1後是否為0,若為0則呼叫
_M_destroy()
刪除控制塊。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
//當前物件的引用計數為0時,釋放管理的資源
//純虛擬函式,取決於釋放策略,由派生類實現
virtual void _M_dispose() noexcept = 0;
//當前物件的弱引用計數為0時,銷燬自身
virtual void _M_destroy() noexcept
{ delete this; }
void _M_release() noexcept
{
// Be race-detector-friendly. For more info see bits/c++config.
_GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
//減少引用計數,並返回-1之前的值
//如果引用計數為0,則釋放管理的資源
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
{
_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);
//減少弱引用計數,並返回-1之前的值
//如果弱引用計數為0,則銷燬控制塊自身
if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1)
{
_GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
_M_destroy();
}
}
}
};
這裡再說明一下為什麼__shared_count
要在引用計數減為0時還要對弱引用計數做減1操作:
在__shared_count
構造的同時,也會構造一個控制塊物件,其中引用計數和弱引用計數一同被初始化為1。這意味著,即使最後一個std::weak_ptr
被銷燬了,但若其對應的std::shared_ptr
還至少存在一個,那麼弱引用計數就不會被減少至0(程式碼中的註釋也是這麼提示的)。
//file: shared_ptr_base.h
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp>
{
_Atomic_word _M_use_count; // #shared
_Atomic_word _M_weak_count; // #weak + (#shared != 0)
};
在std::shared_ptr
物件存在的情況下,所有相關std::weak_ptr
物件被銷燬後,控制塊仍存在,且其中的弱引用計數為1,此時在銷燬最後一個std::shared_ptr
物件時,除了要減少引用計數為0,釋放管理的記憶體資源,還要把最後一個弱引用計數減少為0,銷燬控制塊。
在std::weak_ptr
物件存在的情況下,所有相關std::shared_ptr
物件都被銷燬後,①std::shared_ptr
管理的記憶體資源會被釋放(因為引用計數為0,_M_dispose()
被呼叫)②弱引用計數不為0,控制塊仍然存在(直到最後一個std::weak_ptr
物件被銷燬,控制塊才會被銷燬)
參考文章
1.C++2.0 shared_ptr和weak_ptr深入刨析
2.智慧指標std::weak_ptr