TarsCpp 組件 之 智能指針詳解

做者 Eatonios

導語 在 C++ 中,內存管理是十分重要的問題,一不當心就會形成程序內存泄露,那麼怎麼避免呢?經過智能指針能夠優雅地管理內存,讓開發者只須要關注內存的申請,內存的釋放則會被自動管理。在文章 開源微服務框架 TARS 之 基礎組件 中已經簡要介紹過,TARS 框架組件中沒有直接使用 STL 庫中的智能指針,而是實現了本身的智能指針。本文將會分別對 STL 庫中的智能指針和 TarsCpp 組件中的智能指針進行對比分析,並詳細介紹 TARS 智能指針的實現原理。git

目錄

智能指針

簡介

在計算機程序中,泄露是常見的問題,包括內存泄露和資源泄露。其中資源泄露指的是系統的 socket、文件描述符等資源在使用後,程序再也不須要它們時沒有獲得釋放;內存泄露指的是動態內存在使用後,程序再也不須要它時沒有獲得釋放。安全

內存泄露會使得程序佔用的內存愈來愈多,而很大一部分每每是程序再也不須要使用的。在 C++ 程序中,內存泄露常見於咱們使用了 new 或者 malloc 申請動態存儲區的內存,卻忘了使用 delete 或者 free 去釋放內存,形成系統內存的浪費,致使程序運行速度減慢甚至系統崩潰等嚴重後果。併發

隨着計算機應用需求的日益增長,應用的設計與開發日趨複雜,開發人員在開發過程當中處理的變量也愈來愈多。如何有效進行內存分配和釋放、防止內存泄漏逐漸成爲開發者面臨的重要難題。爲了解決忘記手動釋放內存形成的內存泄露問題,智能指針誕生了。框架

常見的智能指針的使用場景,包括類中的成員變量(指針型)和普通的變量(指針型)。智能指針能夠實現指針指向對象的共享,而無需關注動態內存的釋放。通用實現技術是引用計數(Reference count),下一部分會介紹,簡單講就是將一個計數器與類指向的對象相關聯,跟蹤有多少個指針指向同一對象,新增一個指針指向該對象則計數器 +1,減小一個則執行 -1socket

引用計數原理

引用計數是智能指針的一種通用實現技術,上圖爲大體流程,基本原理以下:分佈式

  1. 在每次建立類的新對象時,初始化指針並將引用計數置 1
  2. 當對象做爲另外一對象的副本而建立時(複製構造函數),複製對應的指針並將引用計數 +1
  3. 當對一個對象進行賦值時,賦值操做符 = 將左操做數所指對象的引用計數 -1,將右操做數所指對象的引用計數 +1
  4. 調用析構函數數,引用計數 -1
  5. 上述操做中,引用計數減至 0 時,刪除基礎對象;

STL 庫中的智能指針 shared_ptr 和 TARS 智能指針都使用了該引用計數原理,後面會進行介紹。

STL 庫的智能指針

C++ 標準模板庫 STL 中提供了四種指針 auto_ptr, unique_ptr, shared_ptr, weak_ptr

auto_ptr 在 C++98 中提出,但其不能共享對象、不能管理數組指針,也不能放在容器中。所以在 C++11 中被摒棄,並提出 unique_ptr 來替代,支持管理數組指針,但不能共享對象。

shared_ptrweak_ptr 則是 C++11 從標準庫 Boost 中引入的兩種智能指針。shared_ptr 用於解決多個指針共享一個對象的問題,但存在循環引用的問題,引入 weak_ptr 主要用於解決循環引用的問題。

接下來將詳細介紹 shared_ptr,關於其它智能指針的更多信息和用法請讀者自行查閱。

shared_ptr

shared_ptr 解決了在多個指針間共享對象全部權的問題,最初實現於 Boost 庫中,後來收錄於 C++11 中,成爲了標準的一部分。shared_ptr 的用法以下

#include <memory>
#include <iostream>
using namespace std;

class A 
{
public:
    A() {};
    ~A()
    {
        cout << "A is destroyed" << endl;
    }
};

int main()
{
    shared_ptr<A> sptrA(new A);
    cout << sptrA.use_count() << endl;
    {
        shared_ptr<A> cp_sptrA = sptrA;
        cout << sptrA.use_count() << endl;
    }
    cout << sptrA.use_count() << endl;
    return 0;
}

上述代碼的意思是 cp_sptrA 聲明並賦值後,引用計數增長 1cp_sptrA 銷燬後引用計數 -1,可是沒有觸發 A 的析構函數,在 sprtA 銷燬後,引用計數變爲 0,才觸發析構函數,實現內存的回收。執行結果以下

1
2
1
A is destroyed

shared_ptr 主要的缺陷是遇到循環引用時,將形成資源沒法釋放,下面給出一個示例:

#include <memory>
#include <iostream>
using namespace std;

class B;
class A
{
public:
    A() : m_sptrB(nullptr) {};
    ~A()
    {
        cout << " A is destroyed" << endl;
    }
    shared_ptr<B> m_sptrB;
};

class B
{
public:
    B() : m_sptrA(nullptr) {};
    ~B()
    {
        cout << " B is destroyed" << endl;
    }
    shared_ptr<A> m_sptrA;
};

int main( )
{
    {
        shared_ptr<B> sptrB( new B );//sptrB對應的引用計數置爲1
        shared_ptr<A> sptrA( new A );//sptrA對應的引用計數置爲1
        sptrB->m_sptrA = sptrA;//sptrA對應的引用計數變成2,sptrB仍然是1
        sptrA->m_sptrB = sptrB;//sptrB對應的引用計數變成2,sptrA是2
    }
   //退出main函數後,sptrA和sptrB對應的引用計數都-1,變成1,
   //此時A和B的析構函數都不能執行(引用計數爲0才能執行),沒法釋放內存
    return 0;
}

在上述例子中,咱們首先定義了兩個類 ABA 的成員變量是指向 Bshared_ptr 指針,B 的成員變量是指向 Ashared_ptr 指針。

而後咱們建立了 sptrBsptrA 兩個智能指針對象,而且相互賦值。這會形成環形引用,使得 AB 的析構函數都沒法執行(能夠經過 cout 觀測),從而內存沒法釋放。當咱們沒法避免循環使用時,可使用 weak_ptr 來解決,這裏再也不展開,感興趣的讀者能夠自行查閱。

TARS 智能指針 TC_AutoPtr 實現詳解

TARS 誕生於 2008 年,當時 shared_ptr 尚未被收錄到 STL 標準庫中,所以本身實現了智能指針 TC_AutoPtr。TARS 的智能指針主要是對 auto_ptr 的改進,和 share_ptr 的思想基本一致,可以實現對象的共享,也能存儲在容器中。與 shared_ptr 相比,TC_AutoPtr 更加輕量化,擁有更好的性能,本文後續會對比。

在 TARS 中,智能指針類 TC_AutoPtr 是一個模板類,支持拷貝和賦值等操做,其指向的對象必須繼承自智能指針基類 TC_HandleBase ,包含了對引用計數的加減操做。計數採用的是 C++ 標準庫 <atomic> 中的原子計數類型 std::atomic

計數的實現封裝在類 TC_HandleBase 中,開發者無需關注。使用時,只要將須要共享對象的類繼承 TC_HandleBase,而後傳入模板類 TC_AutoPtr 聲明並構造對象便可,以下

#include <iostream>
#include "util/tc_autoptr.h"

using namespace std;

// 繼承 TC_HandleBase
class A : public tars::TC_HandleBase
{
public:
    A() 
    {
        cout << "Hello~" << endl;
    }
    
    ~A() 
    {
        cout << "Bye~" << endl;
    }
};

int main()
{
    // 聲明智能指針並構造對象
    tars::TC_AutoPtr<A> autoA = new A();
    // 獲取計數 1
    cout << autoA->getRef() << endl;
    // 新增共享
    tars::TC_AutoPtr<A> autoA1(autoA);
    // 獲取計數 2
    cout << autoA->getRef() << endl;
}

使用方式和 shared_ptr 類似,能夠經過函數 getRef 獲取當前計數,getRef 定義於 TC_HandleBase 類中。運行結果以下

Hello~
1
2
Bye~

下面咱們將自底向上介紹分析原子計數器 std::atomic、智能指針基類 TC_HandleBase 和智能指針模板類 TC_AutoPtr,並對 TC_AutoPtrshared_ptr 的性能進行簡單的對比測試。

原子計數類 std::atomic

std::atomic 在 C++11 標準庫 <atomic> 中定義。std::atomic 是模板類,一個模板類型爲 T 的原子對象中封裝了一個類型爲 T 的值。

template <class T> struct atomic;

原子類型對象的主要特色就是從不一樣線程訪問不會致使數據競爭(data race)。所以從不一樣線程訪問某個原子對象是良性 (well-defined) 行爲。而一般對於非原子類型而言,併發訪問某個對象(若是不作任何同步操做)會致使未定義 (undifined) 行爲發生。

C++11 標準庫 std::atomic 提供了針對整型(integral)和指針類型的特化實現。下面是針對整型的特化實現的主要部分

template <> struct atomic<integral> {
    ...
    ...
    operator integral() const volatile;
    operator integral() const;
     
    atomic() = default;
    constexpr atomic(integral);
    atomic(const atomic&) = delete;
 
    atomic& operator=(const atomic&) = delete;
    atomic& operator=(const atomic&) volatile = delete;
     
    integral operator=(integral) volatile;
    integral operator=(integral);
     
    integral operator++(int) volatile;
    integral operator++(int);
    integral operator--(int) volatile;
    integral operator--(int);
    integral operator++() volatile;
    integral operator++();
    integral operator--() volatile;
    integral operator--();
    integral operator+=(integral) volatile;
    integral operator+=(integral);
    integral operator-=(integral) volatile;
    integral operator-=(integral);
    integral operator&=(integral) volatile;
    integral operator&=(integral);
    integral operator|=(integral) volatile;
    integral operator|=(integral);
    integral operator^=(integral) volatile;
    integral operator^=(integral);
};

能夠看到重載了大部分整型中經常使用的運算符,包括自增運算符 ++ 和自減運算符 --,能夠直接使用自增或自減運算符直接對原子計數對象的引用值 +1-1

智能指針基類 TC_HandleBase

TC_HandleBase 是 TARS 的智能指針基類,包含兩個成員變量 _atomic_bNoDelete,定義以下

protected:
    /**
     * 計數
     */
    std::atomic<int> _atomic;
    /**
     * 是否自動刪除
     */
    bool             _bNoDelete;

TC_HandleBase,爲 TARS 智能指針模板類 TC_AutoPtr<T> 提供引用計數的相關操做,增長計數和減小計數接口的相關代碼以下

/**
     * @brief 增長計數
     */
    void incRef() { ++_atomic; }

    /**
     * @brief 減小計數
     * 當計數==0時, 且須要刪除數據時, 釋放對象
     */
    void decRef()
    {
        if((--_atomic) == 0 && !_bNoDelete)
        {
            _bNoDelete = true;
            delete this;
        }
    }
    /**
     * @brief 獲取計數.
     * @return int 計數值
     */
    int getRef() const { return _atomic; }

能夠看到,這裏經過整型的原子計數類的對象 _atomic 實現引用計數,管理智能指針指向對象的引用計數。

智能指針模板類 TC_AutoPtr

TC_AutoPtr 的定義及其構造函數和成員變量以下述代碼,成員變量 _ptr 是一個 T* 指針。構造函數初始化該指針並調用了 TC_HandleBase 成員函數 incRef 進行引用計數 +1,這要求類 T 是繼承自 TC_HandleBase 的。

/**
 * @brief 智能指針模板類. 
 * 
 * 能夠放在容器中,且線程安全的智能指針. 
 * 經過它定義智能指針,該智能指針經過引用計數實現, 
 * 能夠放在容器中傳遞.   
 * 
 * template<typename T> T必須繼承於TC_HandleBase 
 */
template<typename T>
class TC_AutoPtr
{
public:
    /**
     * @brief 用原生指針初始化, 計數+1. 
     *  
     * @param p
     */
    TC_AutoPtr(T* p = 0)
    {
        _ptr = p;

        if(_ptr)
        {
            _ptr->incRef();
        }
    }
    ...

public:
    T*  _ptr;
};

TC_AutoPtr 在使用時能夠簡單的看成 STL 的 shared_ptr 使用,須要注意的是指向的對象必須繼承自 TC_HandleBase(固然也能夠本身實現智能指針基類,並提供與 TC_HandleBase 一致的接口),同時還要避免環形引用。下面咱們看一下 TC_AutoPtr 其餘接口的定義:

/**
     * @brief 用其餘智能指針r的原生指針初始化, 計數+1. 
     *  
     * @param Y
     * @param r
     */
    template<typename Y>
    TC_AutoPtr(const TC_AutoPtr<Y>& r)
    {
        _ptr = r._ptr;

        if(_ptr)
        {
            _ptr->incRef();
        }
    }

    /**
     * @brief 拷貝構造, 計數+1. 
     *  
     * @param r
     */
    TC_AutoPtr(const TC_AutoPtr& r)
    {
        _ptr = r._ptr;

        if(_ptr)
        {
            _ptr->incRef();
        }
    }

    /**
     * @brief 析構,計數-1
     */
    ~TC_AutoPtr()
    {
        if(_ptr)
        {
            _ptr->decRef();
        }
    }

    /**
     * @brief 賦值, 普通指針
     * @param p 
     * @return TC_AutoPtr&
     */
    TC_AutoPtr& operator=(T* p)
    {
        if(_ptr != p)
        {
            if(p)
            {
                p->incRef();
            }

            T* ptr = _ptr;
            _ptr = p;

            //因爲初始化時_ptr=NULL,所以計數不會-1
            if(ptr)
            {
                ptr->decRef();
            }
        }
        return *this;
    }

能夠看到,這些接口都知足通用的引用計數規則。

  • 構造函數 :除了初始化指針對象以外,將引用計數 +1
  • 拷貝構造函數:拷貝指針,引用計數 +1
  • 賦值操做符:拷貝指針,操做符右邊的智能指針對應的引用計數 +1,左邊的 -1
  • 析構函數:引用計數 -1

TC_AutoPtr 優點

通過上述分析,能夠發現 TC_AutoPtrshared_ptr 在用法和功能上很是類似,都支持多個指針共享一個對象,支持存儲在容器中,那 TC_AutoPtr 有什麼優點呢?

相比於 STL 庫中的 shared_ptrTC_AutoPtr 更加輕量,具備更好的性能,咱們能夠經過以下簡單的測試代碼,經過測試兩者構造和複製的耗時來衡量它們的性能

#include <iostream>
#include <chrono>
#include <memory>
#include <vector>
#include "util/tc_autoptr.h"

using namespace tars;
using namespace std;
using namespace chrono;

// 測試類
class Test : public TC_HandleBase {
public:
  Test() {}
private:
  int test;
};

// 打印時間間隔
void printDuration(const string & info, system_clock::time_point start, system_clock::time_point end) {
  auto duration = duration_cast<microseconds>(end - start);
  cout << info
       << double(duration.count()) * microseconds::period::num / microseconds::period::den
       << " s" << endl;
}

int main() {
  int exec_times = 10000000; // 次數

  // 構造耗時對比
  {
    auto start = system_clock::now();

    for (int i = 0; i < exec_times; ++i) {
      TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test);
    }

    auto end = system_clock::now();
    printDuration("TC_AutoPtr construct: ", start, end);
  }

  {
    auto start = system_clock::now();

    for (int i = 0; i < exec_times; ++i) {
      shared_ptr<Test> a = shared_ptr<Test>(new Test);
    }

    auto end = system_clock::now();
    printDuration("shared_ptr construct: ", start, end);
  }

  // 複製耗時對比
  {
    auto start = system_clock::now();

    TC_AutoPtr<Test> a = TC_AutoPtr<Test>(new Test);
    for (int i = 0; i < exec_times; ++i) {
      TC_AutoPtr<Test> b = a;
    }

    auto end = system_clock::now();
    printDuration("TC_AutoPtr copy: ", start, end);
  }

  {
    auto start = system_clock::now();

    shared_ptr<Test> a = shared_ptr<Test>(new Test);
    for (int i = 0; i < exec_times; ++i) {
      shared_ptr<Test> b = a;
    }

    auto end = system_clock::now();
    printDuration("shared_ptr copy: ", start, end);
  }
}

最後運行測試,輸出的結果以下

TC_AutoPtr construct: 0.208995 s
shared_ptr construct: 0.423324 s
TC_AutoPtr copy: 0.107914 s
shared_ptr copy: 0.107716 s

能夠看出,兩者的複製性能相近,而構造性能上, TC_AutoPtr 要比 shared_ptr 快一倍以上。

總結

本文主要介紹了 TARS 的智能指針組件 TC_AutoPtr 和 STL 的智能指針 shared_ptrTC_AutoPtr 指向繼承自智能指針基類 TC_HandleBase 的對象。TC_HandleBase 經過原子計數器 std::atomic<int> 實現引用計數,確保引用計數是線程安全的。相比於 shared_ptrTC_AutoPtr 擁有更好的性能;而 shared_ptr 有更加完善的功能。TarsCpp 框架已經支持 C++11,開發者可以根據業務具體需求自由選擇。

TARS能夠在考慮到易用性和高性能的同時快速構建系統並自動生成代碼,幫助開發人員和企業以微服務的方式快速構建本身穩定可靠的分佈式應用,從而令開發人員只關注業務邏輯,提升運營效率。多語言、敏捷研發、高可用和高效運營的特性使 TARS 成爲企業級產品。

TARS微服務助您數字化轉型,歡迎訪問:

TARS官網:https://TarsCloud.org

TARS源碼:https://github.com/TarsCloud

Linux基金會官方微服務免費課程:https://www.edx.org/course/bu...

獲取《TARS官方培訓電子書》:https://wj.qq.com/s2/6570357/...

或掃碼獲取:

QR

相關文章
相關標籤/搜索