[*] Improved destruction watchers

This commit is contained in:
Reece Wilson 2024-03-02 00:47:14 +00:00
parent b4f4e623d8
commit 27977779a9

View File

@ -16,15 +16,17 @@ namespace Aurora::Utility
inline DestructionWatch();
inline ~DestructionWatch();
// If your (optionally abstract) ADestructionWatcher requires access to the parent object, it is highly recommened to call RemoveAll within the destructor of the parent class.
// Otherwise, you must maintain a strict ctor/dtor order of your classes layout.
inline void RemoveAll();
private:
friend struct ADestructionWatcher;
Threading::Waitables::FutexWaitable mutex;
Threading::Waitables::FutexWaitable dtorSignal;
ADestructionWatcher *pFirstWatcher {};
AuList<ADestructionWatcher *> listpWatches;
AuList<ADestructionWatcher *> *pListpWatches {};
AuUInt32 uDtorCalls {};
inline void RemoveWatcher(ADestructionWatcher *pWatcher);
inline void RemoveWatcherCall(ADestructionWatcher *pWatcher);
@ -41,11 +43,38 @@ namespace Aurora::Utility
inline bool IsObservedDead();
protected:
inline virtual void OnDestroyedChild()
// Note that since the parent object is in the midst of destruction; you should not be manually calling the destroy operations of it or its' children.
// C++ construction and the inverse destruction order are well defined.
// In the case of extending DestructionWatch as a base class, our constructor should be invoked first, and therefore our destructor will be invoked last.
// In the case of inserting DestructionWatch as a class member, you must guard resource access via synchronization primitives placed in proper construction/destruction order.
inline virtual void OnParentDestroy()
{
}
inline virtual void OnDestroy()
{
// You *might* be able to access the parent object at this point in time.
//
// It is legal to access the memory under both OnParentDestroy(), and OnDestroy() in the case that IsObservedAlive() return true; however, accessing the parents
// member fields shall be restricted pursuant to the inverse of C++s construction order. It is possible for multithreaded code to be in the midst of calling
// the destruction chain before we are aware of the destruction condition. Shared pointers work around this by invoking private std::enable_shared_from_this
// members at the time of construction and time of control block release. It *should* be safe to access HANDLEs, raw c pointers, etc without any special logic,
// on the account that the parent object will not have released its' memory until we're done; same applies to OnParentDestroy().
//
// ** To further harden against out of order destruction, it is advisable but not required, to call DestructionWatch::RemoveAll() in the parents destructor. **
// ** Alternatively, you must be hyper-aware of the C++ destruction order. **
}
public:
inline void Destroy()
{
if (!DoUnderLock(&ADestructionWatcher::OnDestroy, this))
{
this->OnDestroy();
}
}
private:
friend struct DestructionWatch;
friend class Threading::LockGuard<ADestructionWatcher *>;
@ -60,21 +89,10 @@ namespace Aurora::Utility
AuConditional_t<AuIsSame_v<AuResultOf_t<T, Args...>, void>, bool, AuOptional<AuResultOf_t<T, Args...>>>
DoUnderLock(const T &callable, Args && ...args)
{
AU_LOCK_GUARD(this);
{
AU_LOCK_GUARD(this->selfLock);
if (this->pOwnsLock)
{
if constexpr (AuIsSame_v<AuResultOf_t<T, Args...>, void>)
{
callable(AuForward<Args &&>(args)...);
return true;
}
else
{
return callable(AuForward<Args &&>(args)...);
}
}
else
if (!this->pParent)
{
if constexpr (AuIsSame_v<AuResultOf_t<T, Args...>, void>)
{
@ -85,12 +103,28 @@ namespace Aurora::Utility
return AuOptional<AuResultOf_t<T, Args...>> {};
}
}
AuAtomicAdd(&this->pParent->uDtorCalls, 1u);
}
{
AU_LOCK_GUARD(this);
if constexpr (AuIsSame_v<AuResultOf_t<T, Args...>, void>)
{
AuInvoke(callable, AuForward<Args &&>(args)...);
return true;
}
else
{
return AuInvoke(callable, AuForward<Args &&>(args)...);
}
}
}
private:
Threading::Waitables::FutexWaitable selfLock;
DestructionWatch *pParent {};
Threading::Waitables::FutexWaitable *pOwnsLock {};
};
DestructionWatch::DestructionWatch()
@ -113,14 +147,23 @@ namespace Aurora::Utility
this->RemoveWatcherCall(pWatcher);
}
for (const auto pWatcher : AuExchange(this->listpWatches, {}))
if (auto pWatchList = AuExchange(this->pListpWatches, nullptr))
{
for (const auto pWatcher : *pWatchList)
{
this->RemoveWatcherCall(pWatcher);
}
delete pWatchList;
}
}
{
AU_LOCK_GUARD(this->dtorSignal);
AuUInt32 uOld {};
while ((uOld = AuAtomicLoad(&this->uDtorCalls)) != 0)
{
Aurora::Threading::WaitOnAddress(&this->uDtorCalls, &uOld, sizeof(uOld), 0);
}
}
}
@ -134,7 +177,10 @@ namespace Aurora::Utility
return;
}
AuTryRemove(this->listpWatches, pWatcher);
if (this->pListpWatches)
{
AuTryRemove(*this->pListpWatches, pWatcher);
}
}
void DestructionWatch::RemoveWatcherCall(ADestructionWatcher *pWatcher)
@ -161,7 +207,11 @@ namespace Aurora::Utility
}
else
{
pWatch->listpWatches.push_back(this);
if (!pWatch->pListpWatches)
{
pWatch->pListpWatches = new AuRemovePointer_t<decltype(pWatch->pListpWatches)>();
}
pWatch->pListpWatches->push_back(this);
}
this->pParent = pWatch;
}
@ -187,8 +237,14 @@ namespace Aurora::Utility
if (auto pParent = AuExchange(this->pParent, nullptr))
{
this->OnDestroy();
pParent->RemoveWatcher(this);
}
else
{
this->OnDestroy();
}
}
void ADestructionWatcher::OnDTOR()
@ -198,32 +254,19 @@ namespace Aurora::Utility
this->pParent = nullptr;
}
this->OnDestroyedChild();
this->OnParentDestroy();
}
void ADestructionWatcher::Lock()
{
AU_LOCK_GUARD(this->selfLock);
this->pOwnsLock = nullptr;
if (!this->pParent)
{
return;
}
this->pParent->dtorSignal.Lock();
// noting that once we leave this scope, we could have our parent stolen from us (this->selfLock), but its' destructor will still be blocked until we finish
// this is why we end up using a raw pointer here.
this->pOwnsLock = &this->pParent->dtorSignal;
}
void ADestructionWatcher::Unlock()
{
if (this->pOwnsLock)
if (AuAtomicSub(&this->pParent->uDtorCalls, 0u) == 0)
{
this->pOwnsLock->Unlock();
Aurora::Threading::WakeOnAddress(&this->pParent->uDtorCalls);
}
}
}