C++智慧指標學習——小談引用計數

paw5zx發表於2024-04-07

目錄
  • 前言
  • 控制塊簡介
  • 共享控制塊
  • 引用計數與弱引用計數建立過程
    • __shared_ptr
    • __shared_count
    • _Sp_counted_base
  • 弱引用計數增加過程
    • 再談共享控制塊
    • __weak_count
  • 引用計數增加過程
  • 弱引用計數的減少過程
    • 弱引用計數減為0
  • 引用計數的減少過程
    • 引用計數減為0
  • 參考文章

前言

本文結合原始碼討論std::shared_ptr和std::weak_ptr的部分底層實現,然後討論引用計數,弱引用計數的建立和增減。
文章中儘可能的先闡述原理,然後再貼上程式碼。如果有不想看程式碼的,直接略過程式碼即可。
本文涉及的原始碼均出自gcc 9.4.0版本

控制塊簡介

控制塊是shared_ptrweak_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_ptrstd::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_countshared_ptr的計數,就是引用計數,表示有多少個shared_ptr物件共享同一個記憶體資源。
  • _M_weak_countweak_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_ptrshared_ptr共享控制塊的功能。

__weak_count

弱引用計數的增加可以分為下面幾種情況:

  • 透過std::shared_ptr構造std::weak_ptr
  • 透過std::weak_ptr構造std::weak_ptr
  • 透過std::shared_ptrstd::weak_ptr賦值
  • 透過std::weak_ptrstd::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_ptrstd::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

相關文章