Repository navigation
Weak References Proposal
It would help make LiteCore safer if Fleece supported weak references to RefCounted objects, i.e. a WeakRetained<> / WeakRef<> type. The difference is that
- The existence of a
WeakRefto an object does not prevent that object from being destructed when all strong references are gone. - Dereferencing a
WeakRefwhose object has been destructed returnsnullptr, not a dangling pointer.
Weak references are notoriously tricky to implement in a multithreaded environment. Here's how I plan to do it.
The only visible API changes are the addition of a WeakRetained<> template, and an accessor that returns a RefCounted object's weak ref-count (for debugging purposes.)
WeakRetained<> has almost the same API as Retained<> except that it's missing get(), operator * and operator ->. That's because dereferencing has to be done with caution since the value can unpredictably change to nullptr. Instead there's a method tryGet() that returns a Retained<>. If the object still exists this creates a new strong reference that remains valid as long as the returned Retained<> does. Otherwise the return value will contain nullptr. You have to check.
We give RefCounted a second ref-count, weakCount. It starts at 0. Methods _weakRetain and _weakRelease increment and decrement it. The WeakRetained<> template calls these, the same way that Retained calls _retain / _release.
The magic happens when a release call drops the refCount to 0 while there is a non-zero weakCount. In this situation it does not delete the object; instead it puts it into a "zombie" state by calling its destructor but not freeing the heap block. This way the weak references still point to valid memory. (Calling the destructor makes the object free any auxiliary memory it's holding onto, and -- importantly! -- release any references to other RefCounted objects. This minimizes the amount of left-over memory.)
WeakRetained::tryGet() checks the object's refCount; if it's zero it returns nullptr. Otherwise it calls retain and returns a Retained<> pointing to the object. That ensures that the object will remain intact until the caller is done using it.
The private RefCounted::_weakRelease decrements the weakCount. If it hits zero and the refCount is also zero, it calls operator delete to free the object's heap block.
The hard part is how to make this thread-safe (and fast.)
As before, we use atomic. This time both ref-counts are stored in a single atomic<uint64_t> called _bothCounts: the regular refCount in the lower 32 bits, the weakCount in the upper 32 bits. This allows us to atomically update and view both of them.
We slightly change the interpretation of the regular refCount. Basically, we add 1 to it, so it's initialized to 1, and when it's released back down to 1 the object now has no strong references (but is still alive). A refCount of 0, with a nonzero weakCount, now denotes the zombie state.
Here's what the various RefCounted methods do:
- Initializes refCount to 1 and weakCount to 0.
Postcondition: Object is in the "initialized" state.
Precondition: A strong reference exists on this thread, or the object is in the "initialized" state.
- Increments refCount. (Actually it increments
_bothCountsdirectly, which has the same effect.)
Postcondition: A new strong reference is created.
Precondition: A strong reference exists on this thread
- Decrements & reads refCount. (Actually it decrements
_bothCountsdirectly, which has the same effect.) - If the new value of refCount is 1:
- If weakCount is 0: deletes
thisas usual. - Else:
- Calls the destructor, i.e.
this->~RefCounted(). - Decrements & reads refCount again, leaving it 0 -- this marks the object as a zombie.
- If weakCount is now 0: frees the block by calling
operator delete(this).
- Calls the destructor, i.e.
- If weakCount is 0: deletes
Postcondition: The strong reference no longer exists.
Precondition: A strong or weak reference exists on this thread.
- Increments weakCount (by adding
1 << 32to_bothCounts.)
Postcondition: A new weak reference is created.
Precondition: A weak reference exists on this thread (but will no longer exist upon return.)
- Decrements & reads weakCount (by subtracting
1 << 32from_bothCounts.) - If weakCount is 0 and refCount is 0: calls
operator delete(this).
Postcondition: The weak reference no longer exists.
Precondition: A weak reference exists on this thread.
- Repeat:
- Read
_bothRefCounts. - If the refCount is 0 or 1, return false
- Call
compare_exchange_strong()to add 1 to_bothRefCountsif it still has the same value - If it succeeded: return true.
- Else: restart the loop.
- Read
Postcondition: If
truewas returned, a new strong reference is created; onfalse, the object no longer exists.
The WeakRetained class is mostly identical to Retained except that
- It calls
weakRetainandweakReleaseinstead ofretainandrelease - The
tryGet()method callsweakToStrong.- on success it returns a
Retainedinstance adopting the new strong ref. - on failure it clears itself (releases its weak ref and clears its pointer) and returns
nullptr.
- on success it returns a