c++ 共享指针的实现 skia中的 shared pointer sk_sp

共享指针实现

通过每个共享变量,找到其中的指针,大家都执行一个变量对象(共享的变量对象)。在这个共享变量里面,放着计数器,表示当前被其他变量引用的次数。里面有多线程处理的atomic<int>。

这个计数不能用static成员变量实现。因为static是类的属性。如果同一个类,有两个对象。那就无法分别为这两个对象计数了。

1. 为什么需要智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:new 在动态内存中为对象分配空间并返回一个指向该对象的指针;delete 接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出问题,比如new了一个变量,忘记delete,就会产生内存泄漏问题;有2个指针指向同一块内存,第一个指针成功delete与之关联的内存,那剩下那么指针再次delelte同一块内存,就会产生错误。

为了更容易地使用动态内存,C++11标准库提供了两种智能指针类型来管理动态对象。shared_ptr允许多个指针指向同一个对象,也称共享指针;unique_ptr则独占所指向的指针,也称独占指针。标准库还定义了一个名为wek_ptr的伴随类,它是一种弱定义,指向shared_ptr所管理的对象,它是为了解决共享指针循环引用的问题,该问题不是此文的重点。

2. shared_ptr指针的使用

智能指针也是模板,当创建智能指针时,必须提供指向的类型。

语法 功能
shared_ptr p 可以指向T类型的智能指针
p 将将p用作一个条件判断,若p指向一个对象,则为true
*p 解引用p,获得它指向的对象
p->mem 等价于(*p).mem

3.shared_ptr指针的引用计数

当进行拷贝和赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。在每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值吗,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁,计数器就会递减。当shared_ptr的计数器变为0,它就会自动释放自己管理的对象。

到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现来决定。关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。

4. 为什么不能用static计数?

static int count;

有些同学可能想要在智能指针类中添加一个static成员变量,每新增一个智能指针类,count就加1 。在只有一个对象的时候,该方法是合适的。但是static变量是属于类的,如下图所示,当出现第二个对象,此时新增一个shared_ptr指针,此时内部的count就变成了4,既不满足指向对象1的指针数量,也不满足指向对象2的指针数量。因此我们需要指向对象1的shared_ptr共享一个内部计数器,指向对象2的shared_ptr共享另外一个内部计数器,实现途径见下一节。

 

用它记录共享个数。

mutable std::atomic<int32_t> fRefCnt;

调试用个数放在了private,可以拿到public中。

#ifdef SK_DEBUG
/** Return the reference count. Use only for debugging. */
int32_t getRefCnt() const {
return fRefCnt.load(std::memory_order_relaxed);
}
#endif

 

复制代码
/*
 * Copyright 2006 The Android Open Source Project
 *
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 */

#ifndef SkRefCnt_DEFINED
#define SkRefCnt_DEFINED

#include "include/core/SkTypes.h"

#include <atomic>       // std::atomic, std::memory_order_*
#include <cstddef>      // std::nullptr_t
#include <iosfwd>       // std::basic_ostream
#include <memory>       // TODO: unused
#include <type_traits>  // std::enable_if, std::is_convertible
#include <utility>      // std::forward, std::swap

/** \class SkRefCntBase

    SkRefCntBase is the base class for objects that may be shared by multiple
    objects. When an existing owner wants to share a reference, it calls ref().
    When an owner wants to release its reference, it calls unref(). When the
    shared object's reference count goes to zero as the result of an unref()
    call, its (virtual) destructor is called. It is an error for the
    destructor to be called explicitly (or via the object going out of scope on
    the stack or calling delete) if getRefCnt() > 1.
*/
class SK_API SkRefCntBase {
public:
    /** Default construct, initializing the reference count to 1.
    */
    SkRefCntBase() : fRefCnt(1) {}

    /** Destruct, asserting that the reference count is 1.
    */
    virtual ~SkRefCntBase() {
    #ifdef SK_DEBUG
        SkASSERTF(this->getRefCnt() == 1, "fRefCnt was %d", this->getRefCnt());
        // illegal value, to catch us if we reuse after delete
        fRefCnt.store(0, std::memory_order_relaxed);
    #endif
    }

    /** May return true if the caller is the only owner.
     *  Ensures that all previous owner's actions are complete.
     */
    bool unique() const {
        if (1 == fRefCnt.load(std::memory_order_acquire)) {
            // The acquire barrier is only really needed if we return true.  It
            // prevents code conditioned on the result of unique() from running
            // until previous owners are all totally done calling unref().
            return true;
        }
        return false;
    }

    /** Increment the reference count. Must be balanced by a call to unref().
    */
    void ref() const {
        SkASSERT(this->getRefCnt() > 0);
        // No barrier required.
        (void)fRefCnt.fetch_add(+1, std::memory_order_relaxed);
    }

    /** Decrement the reference count. If the reference count is 1 before the
        decrement, then delete the object. Note that if this is the case, then
        the object needs to have been allocated via new, and not on the stack.
    */
    void unref() const {
        SkASSERT(this->getRefCnt() > 0);
        // A release here acts in place of all releases we "should" have been doing in ref().
        if (1 == fRefCnt.fetch_add(-1, std::memory_order_acq_rel)) {
            // Like unique(), the acquire is only needed on success, to make sure
            // code in internal_dispose() doesn't happen before the decrement.
            this->internal_dispose();
        }
    }

private:

#ifdef SK_DEBUG
    /** Return the reference count. Use only for debugging. */
    int32_t getRefCnt() const {
        return fRefCnt.load(std::memory_order_relaxed);
    }
#endif

    /**
     *  Called when the ref count goes to 0.
     */
    virtual void internal_dispose() const {
    #ifdef SK_DEBUG
        SkASSERT(0 == this->getRefCnt());
        fRefCnt.store(1, std::memory_order_relaxed);
    #endif
        delete this;
    }

    // The following friends are those which override internal_dispose()
    // and conditionally call SkRefCnt::internal_dispose().
    friend class SkWeakRefCnt;

    mutable std::atomic<int32_t> fRefCnt;

    SkRefCntBase(SkRefCntBase&&) = delete;
    SkRefCntBase(const SkRefCntBase&) = delete;
    SkRefCntBase& operator=(SkRefCntBase&&) = delete;
    SkRefCntBase& operator=(const SkRefCntBase&) = delete;
};

#ifdef SK_REF_CNT_MIXIN_INCLUDE
// It is the responsibility of the following include to define the type SkRefCnt.
// This SkRefCnt should normally derive from SkRefCntBase.
#include SK_REF_CNT_MIXIN_INCLUDE
#else
class SK_API SkRefCnt : public SkRefCntBase {
    // "#include SK_REF_CNT_MIXIN_INCLUDE" doesn't work with this build system.
    #if defined(SK_BUILD_FOR_GOOGLE3)
    public:
        void deref() const { this->unref(); }
    #endif
};
#endif

///////////////////////////////////////////////////////////////////////////////

/** Call obj->ref() and return obj. The obj must not be nullptr.
 */
template <typename T> static inline T* SkRef(T* obj) {
    SkASSERT(obj);
    obj->ref();
    return obj;
}

/** Check if the argument is non-null, and if so, call obj->ref() and return obj.
 */
template <typename T> static inline T* SkSafeRef(T* obj) {
    if (obj) {
        obj->ref();
    }
    return obj;
}

/** Check if the argument is non-null, and if so, call obj->unref()
 */
template <typename T> static inline void SkSafeUnref(T* obj) {
    if (obj) {
        obj->unref();
    }
}

///////////////////////////////////////////////////////////////////////////////

// This is a variant of SkRefCnt that's Not Virtual, so weighs 4 bytes instead of 8 or 16.
// There's only benefit to using this if the deriving class does not otherwise need a vtable.
template <typename Derived>
class SkNVRefCnt {
public:
    SkNVRefCnt() : fRefCnt(1) {}
    ~SkNVRefCnt() {
    #ifdef SK_DEBUG
        int rc = fRefCnt.load(std::memory_order_relaxed);
        SkASSERTF(rc == 1, "NVRefCnt was %d", rc);
    #endif
    }

    // Implementation is pretty much the same as SkRefCntBase. All required barriers are the same:
    //   - unique() needs acquire when it returns true, and no barrier if it returns false;
    //   - ref() doesn't need any barrier;
    //   - unref() needs a release barrier, and an acquire if it's going to call delete.

    bool unique() const { return 1 == fRefCnt.load(std::memory_order_acquire); }
    void ref() const { (void)fRefCnt.fetch_add(+1, std::memory_order_relaxed); }
    void  unref() const {
        if (1 == fRefCnt.fetch_add(-1, std::memory_order_acq_rel)) {
            // restore the 1 for our destructor's assert
            SkDEBUGCODE(fRefCnt.store(1, std::memory_order_relaxed));
            delete (const Derived*)this;
        }
    }
    void  deref() const { this->unref(); }

    // This must be used with caution. It is only valid to call this when 'threadIsolatedTestCnt'
    // refs are known to be isolated to the current thread. That is, it is known that there are at
    // least 'threadIsolatedTestCnt' refs for which no other thread may make a balancing unref()
    // call. Assuming the contract is followed, if this returns false then no other thread has
    // ownership of this. If it returns true then another thread *may* have ownership.
    bool refCntGreaterThan(int32_t threadIsolatedTestCnt) const {
        int cnt = fRefCnt.load(std::memory_order_acquire);
        // If this fails then the above contract has been violated.
        SkASSERT(cnt >= threadIsolatedTestCnt);
        return cnt > threadIsolatedTestCnt;
    }

private:
    mutable std::atomic<int32_t> fRefCnt;

    SkNVRefCnt(SkNVRefCnt&&) = delete;
    SkNVRefCnt(const SkNVRefCnt&) = delete;
    SkNVRefCnt& operator=(SkNVRefCnt&&) = delete;
    SkNVRefCnt& operator=(const SkNVRefCnt&) = delete;
};

///////////////////////////////////////////////////////////////////////////////////////////////////

/**
 *  Shared pointer class to wrap classes that support a ref()/unref() interface.
 *
 *  This can be used for classes inheriting from SkRefCnt, but it also works for other
 *  classes that match the interface, but have different internal choices: e.g. the hosted class
 *  may have its ref/unref be thread-safe, but that is not assumed/imposed by sk_sp.
 */
template <typename T> class sk_sp {
public:
    using element_type = T;

    constexpr sk_sp() : fPtr(nullptr) {}
    constexpr sk_sp(std::nullptr_t) : fPtr(nullptr) {}

    /**
     *  Shares the underlying object by calling ref(), so that both the argument and the newly
     *  created sk_sp both have a reference to it.
     */
    sk_sp(const sk_sp<T>& that) : fPtr(SkSafeRef(that.get())) {}
    template <typename U,
              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
    sk_sp(const sk_sp<U>& that) : fPtr(SkSafeRef(that.get())) {}

    /**
     *  Move the underlying object from the argument to the newly created sk_sp. Afterwards only
     *  the new sk_sp will have a reference to the object, and the argument will point to null.
     *  No call to ref() or unref() will be made.
     */
    sk_sp(sk_sp<T>&& that) : fPtr(that.release()) {}
    template <typename U,
              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
    sk_sp(sk_sp<U>&& that) : fPtr(that.release()) {}

    /**
     *  Adopt the bare pointer into the newly created sk_sp.
     *  No call to ref() or unref() will be made.
     */
    explicit sk_sp(T* obj) : fPtr(obj) {}

    /**
     *  Calls unref() on the underlying object pointer.
     */
    ~sk_sp() {
        SkSafeUnref(fPtr);
        SkDEBUGCODE(fPtr = nullptr);
    }

    sk_sp<T>& operator=(std::nullptr_t) { this->reset(); return *this; }

    /**
     *  Shares the underlying object referenced by the argument by calling ref() on it. If this
     *  sk_sp previously had a reference to an object (i.e. not null) it will call unref() on that
     *  object.
     */
    sk_sp<T>& operator=(const sk_sp<T>& that) {
        if (this != &that) {
            this->reset(SkSafeRef(that.get()));
        }
        return *this;
    }
    template <typename U,
              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
    sk_sp<T>& operator=(const sk_sp<U>& that) {
        this->reset(SkSafeRef(that.get()));
        return *this;
    }

    /**
     *  Move the underlying object from the argument to the sk_sp. If the sk_sp previously held
     *  a reference to another object, unref() will be called on that object. No call to ref()
     *  will be made.
     */
    sk_sp<T>& operator=(sk_sp<T>&& that) {
        this->reset(that.release());
        return *this;
    }
    template <typename U,
              typename = typename std::enable_if<std::is_convertible<U*, T*>::value>::type>
    sk_sp<T>& operator=(sk_sp<U>&& that) {
        this->reset(that.release());
        return *this;
    }

    T& operator*() const {
        SkASSERT(this->get() != nullptr);
        return *this->get();
    }

    explicit operator bool() const { return this->get() != nullptr; }

    T* get() const { return fPtr; }
    T* operator->() const { return fPtr; }

    /**
     *  Adopt the new bare pointer, and call unref() on any previously held object (if not null).
     *  No call to ref() will be made.
     */
    void reset(T* ptr = nullptr) {
        // Calling fPtr->unref() may call this->~() or this->reset(T*).
        // http://wg21.cmeerw.net/lwg/issue998
        // http://wg21.cmeerw.net/lwg/issue2262
        T* oldPtr = fPtr;
        fPtr = ptr;
        SkSafeUnref(oldPtr);
    }

    /**
     *  Return the bare pointer, and set the internal object pointer to nullptr.
     *  The caller must assume ownership of the object, and manage its reference count directly.
     *  No call to unref() will be made.
     */
    T* SK_WARN_UNUSED_RESULT release() {
        T* ptr = fPtr;
        fPtr = nullptr;
        return ptr;
    }

    void swap(sk_sp<T>& that) /*noexcept*/ {
        using std::swap;
        swap(fPtr, that.fPtr);
    }

private:
    T*  fPtr;
};

template <typename T> inline void swap(sk_sp<T>& a, sk_sp<T>& b) /*noexcept*/ {
    a.swap(b);
}

template <typename T, typename U> inline bool operator==(const sk_sp<T>& a, const sk_sp<U>& b) {
    return a.get() == b.get();
}
template <typename T> inline bool operator==(const sk_sp<T>& a, std::nullptr_t) /*noexcept*/ {
    return !a;
}
template <typename T> inline bool operator==(std::nullptr_t, const sk_sp<T>& b) /*noexcept*/ {
    return !b;
}

template <typename T, typename U> inline bool operator!=(const sk_sp<T>& a, const sk_sp<U>& b) {
    return a.get() != b.get();
}
template <typename T> inline bool operator!=(const sk_sp<T>& a, std::nullptr_t) /*noexcept*/ {
    return static_cast<bool>(a);
}
template <typename T> inline bool operator!=(std::nullptr_t, const sk_sp<T>& b) /*noexcept*/ {
    return static_cast<bool>(b);
}

template <typename C, typename CT, typename T>
auto operator<<(std::basic_ostream<C, CT>& os, const sk_sp<T>& sp) -> decltype(os << sp.get()) {
    return os << sp.get();
}

template <typename T, typename... Args>
sk_sp<T> sk_make_sp(Args&&... args) {
    return sk_sp<T>(new T(std::forward<Args>(args)...));
}

/*
 *  Returns a sk_sp wrapping the provided ptr AND calls ref on it (if not null).
 *
 *  This is different than the semantics of the constructor for sk_sp, which just wraps the ptr,
 *  effectively "adopting" it.
 */
template <typename T> sk_sp<T> sk_ref_sp(T* obj) {
    return sk_sp<T>(SkSafeRef(obj));
}

template <typename T> sk_sp<T> sk_ref_sp(const T* obj) {
    return sk_sp<T>(const_cast<T*>(SkSafeRef(obj)));
}

#endif
复制代码

 

一个超级简单的示例 共享指针:

#include<iostream>
using namespace std;
//引用计数类
class counter
{
public:
counter() {}
counter(int parCount) :count(parCount) {}
void increaseCount() { count++; }
void decreasCount() { count--; }
int getCount() { return count; }
private:
int count;
};
//智能指针
template<class T>
class SmartPointer
{
public:
explicit SmartPointer(T* pT) :mPtr(pT), pCounter(new counter(1)) {}
explicit SmartPointer() :mPtr(NULL), pCounter(NULL) {}
~SmartPointer() //析构函数,在引用计数为0时,释放原指针内存
{
if (pCounter != NULL)
{
pCounter->decreasCount();
if (pCounter->getCount() == 0)
{
delete pCounter;
delete mPtr;
pCounter = NULL; //将pCounter赋值为NULL,防止悬垂指针
mPtr = NULL;
cout << "delete original pointer" << endl;
}
}
}
SmartPointer(SmartPointer<T>& rh) //拷贝构造函数,引用加1
{
this->mPtr = rh.mPtr;
this->pCounter = rh.pCounter;
this->pCounter->increaseCount();
}
SmartPointer<T>& operator=(SmartPointer<T>& rh) //赋值操作符,引用加1
{
if (this->mPtr == rh.mPtr)
return *this;
this->mPtr = rh.mPtr;
this->pCounter = rh.pCounter;
this->pCounter->increaseCount();
return *this;
}
T& operator*() //重载*操作符
{
return *mPtr;
}
T* operator->() //重载->操作符
{
return mPtr;
}
T* get()
{
return mPtr;
}
private:
T* mPtr;
counter* pCounter;
};
int main(int argc, char* argv[])
{
SmartPointer<int> sp1(new int(10));
SmartPointer<int> sp2 = sp1;
SmartPointer<int> sp3;
sp3 = sp2;
return 0;
}

在skia代码中,可以看的这类代码:

std::unique_ptr<SkCodec> SkJpegCodec::MakeFromStream(std::unique_ptr<SkStream> stream,
Result* result) {
return SkJpegCodec::MakeFromStream(std::move(stream), result, nullptr);
}
stream是个左值,需要转换成右值。传入SkJpegCodec::MakeFromStream的unique指针:
std::unique_ptr<SkCodec> SkJpegCodec::MakeFromStream(std::unique_ptr<SkStream> stream,
Result* result, std::unique_ptr<SkEncodedInfo::ICCProfile> defaultColorProfile)

在共享指针,可以看到

js中:
// data is a TypedArray or ArrayBuffer e.g. from fetch().then(resp.arrayBuffer())
CanvasKit.MakeImageFromEncoded = function (data) {
data = new Uint8Array(data);
var iptr = CanvasKit._malloc(data.byteLength);
CanvasKit.HEAPU8.set(data, iptr);
var img = CanvasKit._decodeImage(iptr, data.byteLength);
if (!img) {
Debug('Could not decode image');
return null;
}
return img;
};
function("_decodeImage", optional_override([](WASMPointerU8 iptr,
size_t length)->sk_sp<SkImage> {
uint8_t* imgData = reinterpret_cast<uint8_t*>(iptr);
sk_sp<SkData> bytes = SkData::MakeFromMalloc(imgData, length);
return SkImage::MakeFromEncoded(std::move(bytes));
}), allow_raw_pointers());

这里用了右值: SkImage::MakeFromEncoded(std::move(bytes))

转给这个函数

sk_sp<SkImage> SkImage::MakeFromEncoded(sk_sp<SkData> encoded,
                                        std::optional<SkAlphaType> alphaType)
如果sk_sp<SkData>这个类构造有移动构造函数的话,就会调用。没有的话,就会调用复制构造函数。
共享指针是没有性能区别的。应该没必要。

C++共享指针四宗罪

今天,我们从其它角度来分析三大智能指针中用的最多的一种,也就是shared_ptr<>

问题描述

在基于C++的大型系统的设计实现中,由于缺乏语言级别的GC支持,资源生存周期往往是一个棘手的问题。系统地解决这个问题的方法无非两种:

  • 使用GC库
  • 使用引用计数

严格地说,引用计数其实也是一种最朴素的GC。相对于现代的GC技术,引用计数的实现简单,但相应地,它也存在着循环引用和线程同步开销等问题。关于这二者孰优孰劣,已经有过很多讨论,在此就不搅这股混水了。

我一直也没有使用过C++的GC库,在实际项目中总是采用引用计数的方案。而作为Boost的拥趸,首选的自然是shared_ptr。

一直以来我也对shared_ptr百般推崇,然而最近的一些项目开发经验却让我在shared_ptr上栽了坑,对C++引用计数也有了一些新的的认识,遂记录在此。

本文主要针对基于boost::shared_ptr的C++引用计数实现方案进行一些讨论。C++引用计数方案往往伴随着用于自动管理引用计数的智能指针。按是否要求资源对象自己维护引用计数,C++引用计数方案可以分为两类:

  • 侵入式:侵入式的引用计数管理要求资源对象本身维护引用计数,同时提供增减引用计数的管理接口。通常侵入式方案会提供配套的侵入式引用计数智能指针。该智能指针通过调用资源对象的引用计数管理接口来自动增减引用计数。COM对象与CComPtr便是侵入式引用计数的一个典型实例。
  • 非侵入式:非侵入式的引用计数管理对资源对象本身没有任何要求,而是完全借助非侵入式引用计数智能指针在资源对象外部维护独立的引用计数。shared_ptr便是基于这个思路。

第一宗罪

初看起来,非侵入式方案由于对资源对象的实现没有任何要求,相较于侵入式方案更具吸引力。然而事实却并非如此。

下面就来分析一下基于shared_ptr的非侵入式引用计数。在使用shared_ptr的引用计数解决方案中,引用计数完全由shared_ptr控制,资源对象对与自己对应的引用计数一无所知。

而引用计数与资源对象的生存期息息相关,这就意味着资源对象丧失了对生存期的控制权,将自己的生杀大权拱手让给了shared_ptr。这种情况下,资源对象就不得不依靠至少一个shared_ptr实例来保障自己的生存。换言之,资源对象一旦“沾染”了shared_ptr,就一辈子都无法摆脱!考察以下的简单用例:

用例一:

Resource* p = new CResource;  
{  
    shared_ptr q(p);  
}  
p->Use() // CRASH   

单纯为了解决上述的崩溃,可以自定义一个什么也不做的deleter:

 struct noop_deleter {  
    void operator()(void*) {  
        // NO-OP  
    }  
};  

然后将上述用例的第三行改为:

shared_ptr q(p, noop_deleter());

但是这样一来,shared_ptr就丧失了借助RAII自动释放资源的能力,违背了我们利用智能指针自动管理资源生存期的初衷(话说回来,这倒并不是说noop_deleter这种手法毫无用处,Boost.Asio中就巧妙地利用shared_ptr、weak_ptr和noop_deleter来实现异步I/O事件的取消)。

从这个简单的用例可以看出,shared_ptr就像是毒品一样,一旦沾染就难以戒除。更甚者,染毒者连换用其他“毒品”的权力都没有:shared_ptr的引用计数管理接口是私有的,无法从shared_ptr之外操控,也就无法从shared_ptr迁移到其他类型的引用计数智能指针。

不仅如此,资源对象沾染上shared_ptr之后,就只能使用最初的那个shared_ptr实例的拷贝来维系自己的生存期。考察以下用例:用例二:

 {  
    shared_ptr p1(new CResource);  
    shared_ptr p2(p1);            // OK  
    CResource* p3 = p1.get();  
    shared_ptr p4(p3);            // ERROR  
                                  // CRASH  
}   

该用例的执行过程如下:

  1. p1在构造的同时为资源对象创建了一份外部引用计数,并将之置为1
  2. p2拷贝自p1,与p1共享同一个引用计数,将之增加为2
  3. p4并非p1的拷贝,因此在构造的同时又为资源对象创建了另外一个外部引用计数,并将之置为1
  4. 在作用域结束时,p4析构,由其维护的额外的引用计数降为0,导致资源对象被析构
  5. 然后p2析构,对应的引用计数降为1
  6. 接着p1析构,对应的引用计数也归零,于是p1在临死之前再次释放资源对象 最后,由于资源对象被二次释放,程序崩溃

至此,我们已经认识到了shared_ptr的第一宗罪——传播毒品

  • 毒性一:一旦开始对资源对象使用shared_ptr,就必须一直使用
  • 毒性二:无法换用其他类型的引用计数之智能指针来管理资源对象生存期
  • 毒性三:必须使用最初的shared_ptr实例拷贝来维系资源对象生存期

第二宗罪

乘胜追击,再揭露一下shared_ptr的第二宗罪——散布病毒。有点耸人听闻了?其实道理很简单:由于使用了shared_ptr的资源对象必须仰仗shared_ptr的存在才能维系生存期,这就意味着使用资源的客户对象也必须使用shared_ptr来持有资源对象的引用——于是shared_ptr的势力范围成功地从资源对象本身扩散到了资源使用者,侵入了资源客户对象的实现

同时,资源的使用者往往是通过某种形式的资源分配器来获取资源。自然地,为了向客户转交资源对象的所有权,资源分配器也不得不在接口中传递shared_ptr,于是shared_ptr也会侵入资源分配器的接口

有一种情况可以暂时摆脱shared_ptr,例如:

shared_ptr AllocateResource() {  
    shared_ptr pResource(new CResource);  
    InitResource(pResource.get());  
    return pResource;  
}     
void InitResource(IResource* r) {  
    // Do resource initialization...  
}

以上用例中,在InitResource的执行期间,由于AllocateResource的堆栈仍然存在,pResource不会析构,因此可以放心的在InitResource的参数中使用裸指针传递资源对象。

这种基于调用栈的引用计数优化,也是一种常用的手段。但在InitResource返回后,资源对象终究还是会落入shared_ptr的魔掌。由此可以看出,shared_ptr打着“非侵入式”的幌子,虽然没有侵入资源对象的实现,却侵入了资源分配接口以及资源客户对象的实现。

而沾染上shared_ptr就摆脱不掉,如此传播下去,简直就是侵入了除资源对象实现以外的其他各个地方!这不是病毒是什么?

然而,基于shared_ptr的引用计数解决方案真的不会侵入资源对象的实现吗?

第三宗罪

在一些用例中,资源对象的成员方法(不包括构造函数)需要获取指向对象自身,即包含了this指针的shared_ptr

也就是说资源自身,要获取自己的this指针。而这个指针的引用个数在shared_ptr存着。这就引申出enable_shared_from_this。

Boost.Asio的chat示例便展示了这样一个用例:chat_session对象会在其成员函数中发起异步I/O操作,并在异步I/O操作回调中保存一个指向自己的shared_ptr以保证回调执行时自身的生存期尚未结束。

这种手法在Boost.Asio中非常常见,在不考虑shared_ptr带来的麻烦时,这实际上也是一种相当优雅的异步流程资源生存期处理方法。但现在让我们把注意力集中在shared_ptr上。

通常,使用shared_ptr的资源对象必须动态分配,最常见的就是直接从堆上new出一个实例并交付给一个shared_ptr,或者也可以从某个资源池中分配再借助自定义的deleter在引用计数归零时将资源放回池中。

无论是那种用法,该资源对象的实例在创建出来后,都总是立即交付给一个shared_ptr(记为p)。

有鉴于之前提到的毒性三,如果资源对象的成员方法需要获取一个指向自己的shared_ptr,那么这个shared_ptr也必须是p的一个拷贝——或者更本质的说,必须与p共享同一个外部引用计数。

然而对于资源对象而言,p维护的引用计数是外部的陌生事物,资源对象如何得到这个引用计数并由此构造出一个合法的shared_ptr呢?这是一个比较tricky的过程。为了解决这个问题,Boost提供了一个类模板enable_shared_from_this:

所有需要在成员方法中获取指向this的shared_ptr的类型,都必须以CRTP手法继承自enable_shared_from_this。即:

class CResource :  
    public boost::enable_shared_from_this<CResource>  
{  
    // ...  
};  

接着,资源对象的成员方法就可以使用enable_shared_from_this::shared_from_this()方法来获取所需的指向对象自身的shared_ptr了。问题似乎解决了。

但是,等等!这样的继承体系不就对资源对象的实现有要求了吗?换言之,这不正是对资源对象实现的赤裸裸的侵入吗?这正是shared_ptr的第三宗罪——欺世盗名

第四宗罪

最后一宗罪,是铺张浪费。对了,说的就是性能。

基于引用计数的资源生存期管理,打一出生起就被扣着线程同步开销大的帽子。

早期的Boost版本中,shared_ptr是借助Boost.Thread的mutex对象来保护引用计数。在后期的版本中采用了lock-free的原子整数操作一定程度上降低了线程同步开销。

然而即使是lock-free,本质上也仍然是串行化访问,线程同步的开销多少都会存在。也许有人会说这点开销与引用计数带来的便利相比算不得什么。然而在我们项目的异步服务器框架的压力测试中,大量引用计数的增减操作,一举吃掉了5%的CPU。

换言之,1/20的计算能力被浪费在了与业务逻辑完全无关的引用计数的维护上!而且,由于是异步流程的特殊性,也无法应用上面提及的基于调用栈的引用计数优化。

那么针对这个问题就真的没有办法了吗?

其实仔细检视一下整个异步流程,有些资源虽然会先后被不同的对象所引用,但在其整个生存周期内,每一时刻都只有一个对象持有该资源的引用。

用于数据收发的缓冲区对象就是一个典型。它们总是被从某个源头产生,然后便一直从一处被传递到另一处,最终在某个时刻被回收。

对于这样的对象,实际上没有必要针对流程中的每一次所有权转移都进行引用计数操作,只要简单地在分配时将引用计数置1,在需要释放时再将引用计数归零便可以了。

对于侵入式引用计数方案,由于资源对象自身持有引用计数并提供了引用计数的操作接口,可以很容易地实现这样的优化。

但shared_ptr则不然,shared_ptr把引用计数牢牢地攥在手中,不让外界碰触;外界只有通过shared_ptr的构造函数、析够函数以及reset()方法才能够间接地对引用计数进行操作。

而由于shared_ptr的毒品特性,资源对象无法脱离shared_ptr而存在,因此在转移资源对象的所有权时,也必须通过拷贝shared_ptr的方式进行。

一次拷贝就对应一对引用计数的原子增减操作。对于上述的可优化资源对象,如果在一个流程中被传递3次,除去分配和释放时的2次,还会导致6次无谓的原子整数操作。

整整浪费了300%!

事实证明,在将基于shared_ptr的非侵入式引用计数方案更改为侵入式引用计数方案并施行上述优化后,我们的异步服务器框架的性能有了明显的提升。

结语

最后总结一下shared_ptr的四宗罪:

  • 传播毒品一旦对资源对象染上了shared_ptr,在其生存期内便无法摆脱。
  • 散布病毒在应用了shared_ptr的资源对象的所有权变换的整个过程中的所有接口都会受到shared_ptr的污染。
  • 欺世盗名在enable_shared_from_this用例下,基于shared_ptr的解决方案并非是非侵入式的。
  • 铺张浪费由于shared_ptr隐藏了引用计数的操作接口,只能通过拷贝shared_ptr的方式间接操纵引用计数,使得用户难以规避不必要的引用计数操作,造成无谓的性能损失。

探明这四宗罪算是最近一段时间的项目设计开发过程的一大收获。写这篇文章的目的不是为了将shared_ptr一棒子打死,只是为了总结基于shared_ptr的C++非侵入式引用计数解决方案的缺陷,也让自己不再盲目迷信shared_ptr。

作者:liancheng

原文链接:http://blog.liancheng.info/?p=85

posted @   Bigben  阅读(855)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2021-03-08 堆排序笔记
2019-03-08 硅谷夜谈
2019-03-08 指路Reactive Programming
2019-03-08 Spring WebFlux 要革了谁的命?
2018-03-08 我们如何用Go来处理每分钟100万复杂请求的场景
2018-03-08 Docker系列教程05 容器常用命令
2013-03-08 Ubuntu 用户安装文件较器meld使用,以及添加进右键菜单
点击右上角即可分享
微信分享提示