设计高效的线程安全引用计数系统。
本文描述了一个高效的线程安全引用计数系统。
背景
本文使用了C++的一些高级概念,例如可变参数模板、完美转发、就地新建。
引言
引用计数是一种众所周知的方法,可以自动处理具有复杂依赖关系对象的生命周期。C++标准库提供了std::shared_ptr类,它是一个智能指针,用于保留动态分配对象的共享所有权。该类维护内部引用计数器,但不允许直接访问它们。计数器只能通过创建新的智能指针来更改,这限制了它们在C++语言中的应用。然而,在某些情况下需要自定义引用计数系统。Diligent Engine是一个跨平台渲染引擎。它被打包到动态库中,可以在任何语言或系统中使用,因此需要提供简单高效的对象生命周期管理方法。该系统可以使用C++智能指针实现,但这需要引入额外的抽象层:引擎必须公开智能指针而不是原生接口,这是不希望的。除此之外,实现自己的资源计数系统可以提供更大的灵活性和更好的优化可能性。Diligent Engine出于性能原因实现了自己的原子操作。在Win32平台上,Diligent Engine和标准C++原子操作都使用InterlockedIncrement() Windows函数,但由于DE引入的开销较少,其实现比C++标准库提供的实现快两倍以上。
基本引用计数系统
一个非常基本的资源计数系统可以使用看起来像这样的基类来实现
template<typename Base>
class RefCountedObject : public Base
{
public:
void AddRef()
{
++m_NumReferences;
}
void Release()
{
if(--m_NumReferences == 0)
{
delete this;
}
}
private:
virtual ~RefCountedObject() = 0;
int m_NumReferences = 0;
};
该类定义了虚析构函数,允许通过基类指针删除派生类的任何实例。该类计算未完成引用的数量,并在最后一个引用被释放时销毁自身。这种实现提供了非常有限的功能,并且只在单线程场景中工作。真正的资源计数系统应该支持更多功能
- 允许不同类型的内存分配和释放方法
- 提供实现非拥有引用的弱指针
- 对象所有权灵活性(一个对象可能是另一个引用计数对象的一部分)
- 提供异常和多线程安全
- 效率
设计一个满足上述所有要求的系统是一项有趣且具有挑战性的任务。本文提出了Diligent Engine中实现的解决方案。
概述
Diligent Engine实现了支持弱指针的线程安全引用计数系统。它包括以下类和接口
IObject
是引用计数对象的基接口IReferenceCounters
是实现引用计数并控制对象生命周期的辅助对象的接口
RefCountedObject
是所有引用计数对象的模板基类RefCountersImpl
是实现IReferenceCounters
接口定义的实际引用计数功能的模板类MakeNewRCObj
是负责创建对象+引用计数器对的模板类RefCntAutoPtr
是实现强指针的模板类RefCntWeakPtr
是实现弱指针的模板类
下图说明了系统的不同组件
IReferenceCounters 接口
IReferenceCounters
接口的声明如下所示
class IReferenceCounters
{
public:
virtual Atomics::Long AddStrongRef() = 0;
virtual Atomics::Long ReleaseStrongRef() = 0;
virtual Atomics::Long AddWeakRef() = 0;
virtual Atomics::Long ReleaseWeakRef() = 0;
virtual void GetObject(class IObject **ppObject) = 0;
virtual Atomics::Long GetNumStrongRefs()const = 0;
virtual Atomics::Long GetNumWeakRefs()const = 0;
};
该接口定义了以下函数
AddStrongRef()
- 将强引用数量增加1
。该方法返回递增计数器后的强引用数量。它是线程安全的,不需要显式同步。ReleaseStrongRef()
- 将强引用数量减少1
,并在计数器达到零时销毁引用的对象。如果没有更多弱引用,则销毁引用计数器对象本身。该方法是线程安全的,不需要显式同步。AddWeakRef()
- 将弱引用数量增加1
,并返回递增计数器后的弱引用数量。该方法是线程安全的,不需要显式同步。ReleaseWeakRef()
- 将弱引用数量减少1
。如果没有更多强引用和弱引用,则销毁引用计数器对象本身。GetObject()
- 获取引用对象IUnknown
接口的指针。如果对象已被销毁,将返回nullptr
。如果对象仍然存在,将返回对象IUnknown
接口的指针。在这种情况下,对象的强引用数量将增加1
。GetNumStrongRefs()
- 返回未完成的强引用数量。此方法仅用于调试目的。在多线程环境中,返回的数字可能不可靠,因为其他线程可能同时更改计数器的实际值。唯一可靠的值是0
,因为当最后一个强引用被释放时,对象被销毁。GetNumWeakRefs()
- 返回未完成的弱引用数量。此方法仅用于调试目的。在多线程环境中,返回的数字可能不可靠,因为其他线程可能同时更改计数器的实际值。
IObject 接口
IObject
接口的声明如下所示
class IObject
{
public:
virtual void QueryInterface
( const Diligent::INTERFACE_ID &IID, IObject **ppInterface ) = 0;
virtual Atomics::Long AddRef() = 0;
virtual Atomics::Long Release() = 0;
virtual IReferenceCounters* GetReferenceCounters()const = 0;
};
该接口定义了以下函数
QueryInterface()
- 查询特定接口。该方法将强引用数量增加1
。不再需要时,必须通过调用Release()
方法释放接口。AddRef()
- 将强引用数量增加1
。此方法等同于GetReferenceCounters()->AddStrongRef()
。它是线程安全的,不需要显式同步。Release()
- 将强引用数量减少1
,并在计数器达到零时销毁对象。此方法等同于GetReferenceCounters()->ReleaseStrongRef()
。它是线程安全的,不需要显式同步。GetReferenceCounters()
- 返回关联引用计数器对象的IReferenceCounters
接口指针。该方法不会增加返回对象的强引用数量。
RefCountedObject 类
RefCountedObject
是实现所有引用计数对象通用功能的模板基类。其源代码如下所示
/// Base class for all reference counting objects
template<typename Base>
class RefCountedObject : public Base
{
public:
RefCountedObject(IReferenceCounters *pRefCounters)noexcept :
m_pRefCounters( ValidatedCast<RefCountersImpl>(pRefCounters) )
{
}
// Virtual destructor makes sure all derived classes can be destroyed
// through the pointer to the base class
virtual ~RefCountedObject()
{
}
inline virtual IReferenceCounters* GetReferenceCounters()const override final
{
return m_pRefCounters;
}
inline virtual Atomics::Long AddRef()override final
{
// Since type of m_pRefCounters is RefCountersImpl,
// this call will not be virtual and should be inlined
return m_pRefCounters->AddStrongRef();
}
inline virtual Atomics::Long Release()override final
{
// Since type of m_pRefCounters is RefCountersImpl,
// this call will not be virtual and should be inlined
return m_pRefCounters->ReleaseStrongRef();
}
private:
template<typename AllocatorType, typename ObjectType>
friend class MakeNewRCObj;
friend class RefCountersImpl;
// Operator new is private, and can only be called by MakeNewRCObj
void* operator new(size_t Size)
{
return new Uint8[Size];
}
// This operator delete can only be called from MakeNewRCObj
// if an exception is thrown,
// or from RefCountersImpl when object is destroyed
void operator delete(void *ptr)
{
delete[] reinterpret_cast<Uint8*>(ptr);
}
// Operator new is private, so that an object
// can only be constructed by MakeNewRCObj
template<typename ObjectAllocatorType>
void* operator new(size_t Size, ObjectAllocatorType &Allocator,
const Char* dbgDescription,
const char* dbgFileName, const Int32 dbgLineNumber)
{
return Allocator.Allocate(Size, dbgDescription, dbgFileName, dbgLineNumber);
}
// This operator is only called when an exception is thrown
// from an object constructor
template<typename ObjectAllocatorType>
void operator delete(void *ptr, ObjectAllocatorType &Allocator,
const Char* dbgDescription,
const char* dbgFileName, const Int32 dbgLineNumber)
{
return Allocator.Free(ptr);
}
// Note that the type of the reference counters is RefCountersImpl,
// not IReferenceCounters. This avoids virtual calls from
// AddRef() and Release()
RefCountersImpl *const m_pRefCounters;
};
关于实现的一些评论
AddRef()
、Release()
和GetReferenceCounters()
方法被声明为final
和inline
。第一个关键字告诉编译器这些方法永远不会在派生类中被覆盖,因此编译器可以避免虚调用。inline
是编译器内联这些方法的另一个提示。- 对
AddRef()
和Release()
的调用委托给引用计数器对象,其指针保存在m_pRefCounters
成员中。由于此指针的类型是RefCountersImpl*
而不是IReferenceCounters*
,一个好的编译器将能够避免虚调用并可能内联它们。 - 必须向构造函数提供有效的引用计数器对象指针。请注意,
m_pRefCounters
不一定必须跟踪对象本身。它可能是直接或间接拥有此对象的另一个对象。 - 该类定义了两个版本的运算符
new
和两个运算符delete
。这些运算符是私有的,只能由负责构造新对象的MakeNewRCObj
类访问。第一对使用标准C++运算符new来分配所需的内存量。第二对使用自定义分配器。这些版本将分配器引用作为参数。它们还使用调试信息,例如分配描述、文件名和执行分配的行号。另请注意,只有在对象构造期间抛出异常时才会调用其中一个运算符delete
。
RefCountersImpl 类
RefCountersImpl
类负责大部分实际的引用计数功能,并实现了IReferenceCounters
接口。
销毁受控对象
RefCountersImpl
类必须支持的最重要操作是销毁它控制的对象。此操作需要足够灵活,以支持不同类型的对象以及不同的分配器。实现此目的的一种方法是将类设置为模板类,由受控对象的类型和分配器类型参数化
template<typename ControlledObjectType, typename ObjectAllocatorType>
class RefCountersImpl : public IReferenceCounters
这样的模板类可以持有指向对象和分配器的指针
ControlledObjectType* m_pObject;
ObjectAllocatorType * const m_pObjectAllocator;
这样就可以如下销毁对象
auto *pObj = m_pObject;
auto *pAllocator = m_pObjectAllocator;
// ...
if (pAllocator)
{
pObj->~ControlledObjectType();
pAllocator->Free(pObj);
}
else
{
delete pObj;
}
只要ControlledObjectType
是派生自RefCountedObject
的类型,它就会有一个虚析构函数,并且会被pObj->~ControlledObjectType()
成功销毁。这种方法也处理任何分配器,因此是处理不同类型对象和不同类型分配器的合法方法。事实上,Diligent Engine最初就是这样实现的。然而,它有一个缺点:RefCountedObject::m_pRefCounters
成员的类型需要是IReferenceCounter
。原因是当一个对象是另一个引用计数对象的一部分时(例如,纹理的默认视图是纹理对象的一部分),它会保留指向父对象引用计数器的指针。由于两个对象的类型不相关,剩下的唯一选择是使用IReferenceCounter
类,它是所有引用计数器的共同祖先。这种方法的最大缺点是RefCountedObject
类的AddRef()
和Release()
方法对特定RefCountersImpl
实例的AddStrongRef()
和ReleaseStrongRef()
执行虚调用,这是次优的。相反,在上面显示的RefCountedObject
类的实现中,RefCountedObject::m_pRefCounters
成员的类型是RefCountersImpl*
(非模板)。RefCountersImpl
类实现的IReferenceCounter
接口的所有方法都标记为final
和inline
,因此一个好的编译器不仅能够消除虚调用,还能通过内联方法完全消除函数调用。
现在的问题是:如果RefCountersImpl
类不依赖于对象和分配器的类型,我们如何提供所需的灵活性?答案是使用对象包装器模板类,它知道如何销毁对象。首先,我们将定义一个非模板抽象基类,如下所示
class ObjectWrapperBase
{
public:
virtual void DestroyObject() = 0;
virtual void QueryInterface
( const Diligent::INTERFACE_ID &iid, IObject **ppInterface )=0;
};
该类只提供两个纯virtual
方法:一个用于销毁对象,另一个用于查询指定接口(我们稍后将使用它来实现GetObject()
方法)。因此,只要RefCountersImpl
类保留指向ObjectWrapperBase
类型对象包装器的指针,它就可以像pWrapper->DestroyObject()
一样轻松地销毁受控对象。关于使用特定分配器销毁特定对象的具体细节由下面列表中给出的继承模板类定义
template<typename ObjectType, typename AllocatorType>
class ObjectWrapper : public ObjectWrapperBase
{
public:
ObjectWrapper(ObjectType *pObject, AllocatorType *pAllocator) :
m_pObject(pObject),
m_pAllocator(pAllocator)
{}
virtual void DestroyObject()
{
if (m_pAllocator)
{
m_pObject->~ObjectType();
m_pAllocator->Free(m_pObject);
}
else
{
delete m_pObject;
}
}
virtual void QueryInterface
( const Diligent::INTERFACE_ID &iid, IObject **ppInterface )override final
{
return m_pObject->QueryInterface(iid, ppInterface);
}
private:
// It is crucially important that the type of the pointer
// is ObjectType and not IObject, since the latter
// does not have virtual dtor.
ObjectType* const m_pObject;
AllocatorType * const m_pAllocator;
};
您可以看到所有对象销毁逻辑都移动到了DestroyObject()
virtual
方法中。由于该类由对象类型和分配器参数化(与RefCountersImpl
类最初的参数相同),它可以处理所有对象类型和所有分配器类型。我们唯一需要回答的问题是如何初始化ObjectWrapper
类的特定实例。为此,RefCountersImpl
类提供了模板方法Attach()
,它具有相同的模板参数并初始化ObjectWrapper
类的特定实例。但是,该方法不是在堆上创建类,而是在RefCountersImpl
类提供的原始内存缓冲区中就地初始化它
static const size_t ObjectWrapperBufferSize =
sizeof(ObjectWrapper<IObject, IMemoryAllocator>) / sizeof(size_t);
size_t m_ObjectWrapperBuffer[ObjectWrapperBufferSize];
template<typename ObjectType, typename AllocatorType>
void Attach(ObjectType *pObject, AllocatorType *pAllocator)
{
new(m_ObjectWrapperBuffer) ObjectWrapper<ObjectType, AllocatorType>
(pObject, pAllocator);
m_ObjectState = ObjectState::Alive;
}
m_ObjectWrapperBuffer
缓冲区的典型大小将是三个指针的大小(vtbl
指针加上m_pObject
和m_pAlloctor
成员)。就地new
操作
new(m_ObjectWrapperBuffer) ObjectWrapper<ObjectType, AllocatorType>(pObject, pAllocator);
使用ObjectWrapper
类的实例初始化原始内存,其m_pObject
和m_pAlloctor
成员将存储从调用函数提供给构造函数的指针。此外,编译器将为ObjectType
和AllocatorType
类型生成特定的DestroyObject()
方法实例,这将通过ObjectWrapper
类的虚表访问,该指针将依次存储在m_ObjectWrapperBuffer
缓冲区的第一个元素中。因此,总而言之,RefCountersImpl::ReleaseStrongRef()
将执行以下代码来销毁受控对象
// Copy the object wrapper and release the object after unlocking the
// reference counters
size_t ObjectWrapperBufferCopy[ObjectWrapperBufferSize];
for(size_t i=0; i < ObjectWrapperBufferSize; ++i)
ObjectWrapperBufferCopy[i] = m_ObjectWrapperBuffer[i];
auto *pWrapper = reinterpret_cast<ObjectWrapperBase*>(ObjectWrapperBufferCopy);
//...
pWrapper->DestroyObject();
类成员和方法
现在我们描述了RefCountersImpl
类如何管理使用不同分配器分配的各种类型对象,我们可以描述类方法的实现。但首先,让我们看看该类定义的其他成员
Atomics::AtomicLong m_lNumStrongReferences;
Atomics::AtomicLong m_lNumWeakReferences;
ThreadingTools::LockFlag m_LockFlag;
enum class ObjectState : Int32
{
NotInitialized,
Alive,
Destroyed
}m_ObjectState = ObjectState::NotInitialized;
m_lNumStrongReferences
是强引用计数器m_lNumWeakReferences
是弱引用计数器m_LockFlag
是用于获取对象成员独占访问权的锁标志m_ObjectState
是对象的状态(未初始化、活动或已销毁)
现在让我们看看类方法的实现。AddStrongRef()
、AddWeakRef()
、GetNumStrongRefs()
和GetNumWeakRefs()
都很直接
inline virtual Atomics::Long AddStrongRef()override final
{
return Atomics::AtomicIncrement(m_lNumStrongReferences);
}
inline virtual Atomics::Long AddWeakRef()override final
{
return Atomics::AtomicIncrement(m_lNumWeakReferences);
}
inline virtual Atomics::Long GetNumStrongRefs()const override final
{
return m_lNumStrongReferences;
}
inline virtual Atomics::Long GetNumWeakRefs()const override final
{
return m_lNumWeakReferences;
}
请注意,inline
和final
关键字对一个好的编译器来说是非常强的提示,这些方法在使用RefCountersImpl*
类型的指针调用时需要内联。
实际工作由ReleaseStrongRef()
、ReleaseWeakRef()
和GetObject()
方法完成。乍一看,实现可能看起来很简单,但在多线程环境中,可能会有很多棘手的情况。考虑几个例子
- 只剩下一个强引用和一个弱引用。一个线程释放最后一个强引用,而第二个线程释放弱引用。第一个线程销毁受控对象,但哪个线程应该销毁引用计数器本身?如果线程简单地检查两个计数器是否都已达到
0
,那么对象将被释放两次,这将导致未定义行为。 - 只剩下一个强引用和一个弱引用。一个线程释放最后一个强引用,而第二个线程尝试通过调用
GetObject()
获取对象。在这种情况下谁胜出?如何避免返回对已销毁对象的引用?
这只是几个例子,但存在更多复杂的情况。现在我们将给出所有三个函数的实现以供参考,然后对每个函数进行详细解释
inline virtual Atomics::Long ReleaseStrongRef()override final
{
// Decrement strong reference counter without acquiring the lock.
auto RefCount = Atomics::AtomicDecrement(m_lNumStrongReferences);
if( RefCount == 0 )
{
// Acquire the lock.
ThreadingTools::LockHelper Lock(m_LockFlag);
// Copy the object wrapper and release the object after unlocking the
// reference counters
size_t ObjectWrapperBufferCopy[ObjectWrapperBufferSize];
for(size_t i=0; i < ObjectWrapperBufferSize; ++i)
ObjectWrapperBufferCopy[i] = m_ObjectWrapperBuffer[i];
auto *pWrapper = reinterpret_cast<ObjectWrapperBase*>(ObjectWrapperBufferCopy);
// Mark object as destroyed
m_ObjectState = ObjectState::Destroyed;
bool bDestroyThis = m_lNumWeakReferences == 0;
// Unlock the object now to avoid deadlocks
Lock.Unlock();
// Destroy referenced object
pWrapper->DestroyObject();
if( bDestroyThis )
SelfDestroy();
}
return RefCount;
}
inline virtual Atomics::Long ReleaseWeakRef()override final
{
// The method must be serialized!
ThreadingTools::LockHelper Lock(m_LockFlag);
// It is essentially important to check the number of weak references
// while holding the lock. Otherwise reference counters object
// may be destroyed twice if ReleaseStrongRef() is executed by other
// thread.
auto NumWeakReferences = Atomics::AtomicDecrement(m_lNumWeakReferences);
if( NumWeakReferences == 0 && m_ObjectState == ObjectState::Destroyed )
{
Lock.Unlock();
SelfDestroy();
}
return NumWeakReferences;
}
inline virtual void GetObject( class IObject **ppObject )override final
{
if( m_ObjectState != ObjectState::Alive)
return; // Early exit
ThreadingTools::LockHelper Lock(m_LockFlag);
auto StrongRefCnt = Atomics::AtomicIncrement(m_lNumStrongReferences);
if( m_ObjectState == ObjectState::Alive && StrongRefCnt > 1 )
{
auto *pWrapper = reinterpret_cast<ObjectWrapperBase*>(m_ObjectWrapperBuffer);
pWrapper->QueryInterface(Diligent::IID_Unknown, ppObject);
}
Atomics::AtomicDecrement(m_lNumStrongReferences);
}
现在让我们看看这些函数是如何工作的。我们从ReleaseStrongRef()
开始。该函数首先在不获取锁的情况下原子地递减强引用计数器。这很重要,因为加锁是一个昂贵的操作,您只想在绝对必要时才支付此成本。如果递减后的值为0
,这意味着该函数释放了最后一个引用,对象应该被销毁。请注意,Atomics::AtomicDecrement()
原子地递减计数器。这意味着如果多个线程同时到达该指令,对计数器的访问将被序列化。该函数返回递减后的值,并且只有一个线程将读取0
。请注意,使用函数返回的值至关重要,因为如果我们使用m_lNumStrongReferences
进行比较,由于多个线程可能读取0
,对象可能会被销毁多次。读取0
的线程开始销毁对象
auto RefCount = Atomics::AtomicDecrement(m_lNumStrongReferences);
if( RefCount == 0 )
{
// Start destroying the object...
ReleaseStrongRef() 和 GetObject() 方法之间的干扰
这是第一个我们需要仔细设计与其他函数干扰的情况。如果另一个线程有一个弱引用并通过GetObject()
开始获取对象的强引用,会发生什么?如果不特别注意,该方法可能会返回一个对象引用,而该对象很快就会被另一个线程的ReleaseStrongRef()
销毁。在ReleaseStrongRef()
开始销毁对象后,它使用锁获取对类的独占访问。GetObject()
也获取锁,所以现在只有一个线程中的一个方法可以运行。由于ReleaseStrongRef()
无法停止并且对象无论如何都将被销毁,因此GetObject()
有责任检测这种情况并避免返回对即将销毁的对象的引用。在获取锁之后,GetObject()
首先增加强引用计数(我们稍后将讨论为什么在拥有对计数器的独占访问权时增加计数器很重要)并检查返回的值。现在有两种情况
- 返回值为
1
。这意味着不再有活动对象的强引用,并且对象要么已经被销毁,要么很快就会被销毁。在这种情况下,我们不应该返回对象的引用。 - 返回的值大于
1
。这意味着至少存在另一个对对象的强引用。由于我们已经增加了计数器,其他线程中的ReleaseStrongRef()
将无法将其递减到0
(假设对AddRef()
和Release()
的调用是正确平衡的),因此可以安全地返回对对象的引用。
请注意,QueryInterface()
也会增加强引用计数器,但在调用该方法之前,我们需要确保对象是活动的。
以下两幅图说明了两种可能的情景,它们显示了当一个线程正在释放对象的最后一个强引用时,而第二个线程正在尝试通过弱引用使用GetObject()
获取对象的强引用时会发生什么。在第一种情景中,ReleaseStrongRef()
首先递减强引用计数器,然后对象将被释放
在第二种情况下,GetObject()
首先增加强引用计数器,对象保持活动状态
请注意,GetObject()
只有在获取锁之后才增加强引用计数器是至关重要的。考虑如果 GetObject()
没有获取锁,并且有多个线程正在运行 GetObject()
,而另一个线程正在释放对象的最后一个引用时可能发生的情况
如您所见,在上述场景中,两个运行GetObject()
的线程增加了强引用计数器。结果,第二个增加计数器的线程看到两个强引用并开始返回对象。然而,此时对象可能已经被销毁,或者很快将被运行ReleaseStrongRef()
的线程销毁。如果GetObject()
获取锁,则此场景不可能发生
关键的区别在于,如果获取了锁,运行GetObject()
的第二个线程(线程3)只有在运行GetObject()
的第一个线程(线程2)递减了计数器之后才能递增引用计数器。因此,线程3也将只看到一个强引用,并且不会返回对象。在另一种情况下,如果运行GetObject()
的线程之一在ReleaseStrongRef()
递减计数器之前递增了计数器,则对象将保持活动状态,并且线程2和线程3都将获得有效的对象引用。
一个细心的读者此时可能会问两个问题。首先,如果GetObject()
获取了锁,为什么我们还要使用原子操作来增加和减少计数器?这是因为其他方法(AddStrongRef()
和ReleaseStrongRef()
)在不获取锁的情况下访问计数器。第二个问题是:如果AddStrongRef()
在不获取锁的情况下增加引用计数器,是否可能发生与一个线程运行ReleaseStrongRef()
,第二个线程运行GetObject()
,而第三个线程运行AddStrongRef()
时相同的错误场景?答案是不会,因为由于第三个线程运行AddStrongRef()
,至少存在另一个对对象的强引用。所以运行ReleaseStrongRef()
的第一个线程并没有释放最后一个引用。
ReleaseStrongRef() 和 ReleaseWeakRef() 方法之间的干扰
如果不特别注意,如果两个线程通过ReleaseStrongRef()
和ReleaseWeakRef()
方法同时释放最后一个强引用和弱引用,可能会出现问题。如果两个方法都发现没有更多的强引用和弱引用,则采取两个步骤来确保只有一个方法销毁引用计数对象本身。首先,ReleaseStrongRef()
在仍然持有锁时检查强引用数量,并设置一个标志,指示是否需要释放引用计数对象
bool bDestroyThis = m_lNumWeakReferences == 0;
其次,ReleaseWeakRef()
只有在获取锁之后才会递减弱引用计数器。如果两个线程同时运行 ReleaseStrongRef()
和 ReleaseWeakRef()
,根据哪个方法首先获取锁,有两种可能的情况。在第一种情况下,ReleaseStrongRef()
首先获取锁。由于它看到的弱引用数量不为零,该方法将不会销毁引用计数对象
在第二种情况中,ReleaseWeakRef()
先获取锁。然而,ReleaseStrongRef()
必须首先销毁受控对象,因此 ReleaseWeakRef()
不能自销毁引用计数对象。为了检测这种情况,这些方法使用 m_ObjectState
标志。此状态由 ReleaseStrongRef()
方法原子地设置为 ObjectState::Destroyed
。如果 ReleaseWeakRef()
发现状态不是ObjectState::Destroyed,这意味着存在活动的强引用,或者ReleaseStrongRef()
方法尚未完成,如下面的场景所示
请注意,关键是m_ObjectState
在获取锁时访问,并且弱引用计数器也只在获取锁时递减。例如,考虑以下场景,其中弱引用计数器在未获取锁的情况下递减,这导致引用计数器对象自销毁两次
获取锁有两个安全效果
- 如果
ReleaseStrongRef()
将bDestroyThis
标志设置为true
,这意味着没有其他线程可能运行与弱引用相关的代码,因为ReleaseWeakRef()
在获取锁后递减弱引用计数器。因此,引用计数对象可以安全地销毁 - 如果
ReleaseWeakRef()
发现m_ObjectState
被设置为ObjectState::Destroyed
,那么所有与强引用相关的代码此时必须已完成,因为对象状态在保持锁的同时进行了更新。在这种情况下,引用计数器也可以安全地销毁
ReleaseWeakRef() 和 GetObject() 方法之间的干扰
如果正确使用,这两种方法之间实际上不可能发生干扰。如果GetObject()
在一个线程中被调用,而另一个线程正在运行ReleaseWeakRef()
,这意味着至少存在两个弱引用。因此,ReleaseWeakRef()
只会递减计数器,但不会销毁引用计数器对象,因为至少还会有一个未完成的弱引用。
MakeNewRCObj 类
MakeNewRCObj
是一个负责创建对象+引用计数器对的类。该类是一个模板类,由对象类型和内存分配器类型参数化
template<typename ObjectType, typename AllocatorType = IMemoryAllocator>
class MakeNewRCObj
该类定义了以下private
成员
AllocatorType* const m_pAllocator;
IObject* const m_pOwner;
const Char* const m_dbgDescription;
const char* const m_dbgFileName;
const Int32 m_dbgLineNumber;
其中
m_pAllocator
是指向将用于为对象分配内存的分配器的指针。如果为nullptr
,则使用默认系统分配器。m_pOwner
是指向将要创建的对象的拥有者的指针。如果为nullptr
,则该对象没有拥有者。m_dbgDescription
、m_dbgFileName
和m_dbgLineNumber
是提供给分配器用于描述分配以进行调试的调试成员。
该类提供了两个构造函数来初始化其成员
MakeNewRCObj(AllocatorType &Allocator,
const Char* dbgDescription,
const char* dbgFileName,
const Int32 dbgLineNumber,
IObject* pOwner = nullptr)noexcept :
m_pAllocator(&Allocator),
m_pOwner(pOwner),
m_dbgDescription(dbgDescription),
m_dbgFileName(dbgFileName),
m_dbgLineNumber(dbgLineNumber)
{
}
MakeNewRCObj(IObject* pOwner = nullptr)noexcept :
m_pAllocator(nullptr),
m_pOwner(pOwner),
m_dbgDescription(nullptr),
m_dbgFileName(nullptr),
m_dbgLineNumber(0)
{}
该类定义了模板operator ()
,用于执行对象的分配
template<typename ... CtorArgTypes>
ObjectType* operator() (CtorArgTypes&& ... CtorArgs)
{
RefCountersImpl *pNewRefCounters = nullptr;
IReferenceCounters *pRefCounters = nullptr;
if(m_pOwner != nullptr)
pRefCounters = m_pOwner->GetReferenceCounters();
else
{
// Constructor of RefCountersImpl class is private and only accessible
// by methods of MakeNewRCObj
pNewRefCounters = new RefCountersImpl();
pRefCounters = pNewRefCounters;
}
ObjectType *pObj = nullptr;
try
{
// Operators new and delete of RefCountedObject are private and only accessible
// by methods of MakeNewRCObj
if(m_pAllocator)
pObj = new(*m_pAllocator, m_dbgDescription, m_dbgFileName, m_dbgLineNumber)
ObjectType(pRefCounters, std::forward<CtorArgTypes>(CtorArgs)... );
else
pObj = new ObjectType
( pRefCounters, std::forward<CtorArgTypes>(CtorArgs)... );
if(pNewRefCounters != nullptr)
pNewRefCounters->Attach<ObjectType, AllocatorType>(pObj, m_pAllocator);
}
catch (...)
{
if(pNewRefCounters != nullptr)
pNewRefCounters->SelfDestroy();
throw;
}
return pObj;
}
关于操作符有一些有趣的事情。首先,它是一个可变参数模板函数。它可以接受任意数量的任意类型的参数。该函数使用完美转发机制将所有参数传递给对象构造函数
ObjectType(pRefCounters, std::forward<CtorArgTypes>(CtorArgs)... )
完美转发确保每个参数都根据其原始类型以左值或右值形式传递。其次,如果提供了自定义分配器,该方法使用就地new
来创建对象
pObj = new(*m_pAllocator, m_dbgDescription, m_dbgFileName, m_dbgLineNumber)
ObjectType(pRefCounters, std::forward<CtorArgTypes>(CtorArgs)... );
如果未提供分配器,则使用默认分配器
pObj = new ObjectType( pRefCounters, std::forward<CtorArgTypes>(CtorArgs)... );
回想一下,RefCountedObject
定义了两个private
版本的运算符new
,并且MakeNewRCObj
是RefCountersImpl
的友元类。因此,MakeNewRCObj
是唯一可以访问这些运算符的类,也是唯一可以在堆上创建RefCountersImpl
派生类实例的地方。
对象创建后,该方法使用我们之前讨论过的模板Attach()
方法将此对象附加到引用计数器。
如果对象构造函数抛出异常,该方法会捕获它,销毁引用计数器对象,并重新抛出异常。请注意,在我们最初的实现中,引用计数器对象是由对象构造函数创建的。如果构造函数抛出异常,这会导致内存泄漏,因为析构函数永远不会被调用,内存也永远不会被释放。
存在一个与正确异常处理相关的棘手情况。考虑一个场景,对象A
拥有对象B
,对象B
持有对象A
的弱引用WP。如果A
的构造函数抛出异常,那么弱引用的析构函数可能会尝试销毁引用计数对象。我们知道,这个操作是由MakeNewRCObj
类执行的,我们必须避免两次销毁同一个对象。这里有用的是ReleaseWeakRef()
检查对象的状态是否为ObjectState::Destroyed
。由于对象从未被构造,其状态将为ObjectState::NotInitialized
,并且ReleaseWeakRef()
不会销毁引用计数对象。
定义以下宏是为了方便使用MakeNewRCObj
类
#define NEW_RC_OBJ(Allocator, Desc, Type, ...)\
MakeNewRCObj<Type, typename std::remove_reference<decltype(Allocator)>::type>\
(Allocator, Desc, __FILE__, __LINE__, ##__VA_ARGS__)
借助这些宏,典型的分配如下所示
BufferD3D11Impl *pBufferD3D11 =
NEW_RC_OBJ(m_BufObjAllocator, "BufferD3D11Impl instance", BufferD3D11Impl)
(m_BuffViewObjAllocator, this, BuffDesc, BuffData );
智能指针类
引用计数系统提供了两个智能指针类,RefCntAutoPtr
和RefCntWeakPtr
,它们实现了强引用和弱引用功能。类的实现相对简单,因为它们是RefCountersImpl
和RefCountedObject
类的薄封装。
结论
本文介绍了一个高效引用计数系统的实现。该系统支持用户提供的分配器、弱引用,并且是线程安全的。本文中描述的类的源代码可以在附件档案中找到。它也可以在GitHub上获取。
历史
- 2017年6月17日:初始版本