I was thinking about reference counting based on atomic integers that would be safe from overflow. How to do it?
Please let's not focus on whether such overflow is a realistic problem or not. The task itself got my interest even if not practically important.
Example
Example implementation of reference counting is shown as an example in Boost.Atomic. Based on that example we can extract following sample code:
struct T
{
boost::atomic<boost::uintmax_t> counter;
};
void add_reference(T* ptr)
{
ptr->counter.fetch_add(1, boost::memory_order_relaxed);
}
void release_reference(T* ptr)
{
if (ptr->counter.fetch_sub(1, boost::memory_order_release) == 1) {
boost::atomic_thread_fence(boost::memory_order_acquire);
delete ptr;
}
}
In addition following explanation is given
Increasing the reference counter can always be done with
memory_order_relaxed: New references to an object can only be formed from an existing reference, and passing an existing reference from one thread to another must already provide any required synchronization.It is important to enforce any possible access to the object in one thread (through an existing reference) to happen before deleting the object in a different thread. This is achieved by a "release" operation after dropping a reference (any access to the object through this reference must obviously happened before), and an "acquire" operation before deleting the object.
It would be possible to use
memory_order_acq_relfor thefetch_suboperation, but this results in unneeded "acquire" operations when the reference counter does not yet reach zero and may impose a performance penalty.
EDIT >>>
It seems that Boost.Atomic documentation might be wrong here. The acq_rel might be needed after all.
At least such is the implementation of boost::shared_ptr when done using std::atomic (there are other implementations as well). See file boost/smart_ptr/detail/sp_counted_base_std_atomic.hpp.
Also Herb Sutter mentions it in his lecture C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2 (reference counting part starts at 1:19:51). Also he seems to be discouraging use of fences in this talk.
Thanks to user 2501 for pointing that out in comments below.
<<< END EDIT
Initial attempts
Now the problem is that add_reference as written could (at some point) overflow. And it would do so silently. Which obviously could lead to problems when calling matched release_reference that would prematurely destroy the object. (Provided that add_reference would be then called once again to reach 1.)
I was thinking how to make add_reference detect overflow and fail gracefully without risking anything.
Comparing to 0 once we leave fetch_add will not do since between the two some other thread could call add_reference again (reaching 1) and then release_reference (erroneously destroying the object in effect).
Checking first (with load) will not help either. This way some other thread could add its own reference between our calls to load and fetch_add.
Is this the solution?
Then I thought that maybe we could start with load but only if then we do compare_exchange.
So first we do load and get a local value. If it is std::numeric_limits<boost::uintmax_t>::max() then we fail. add_reference cannot add another reference as all possible are already taken.
Otherwise we make another local value which is the previous local reference count plus 1.
And now we do compare_exchange providing as expected value the original local reference count (this ensures that no other thread modified reference count in the mean time) and as the desired value the incremented local reference count.
Since compare_exchange can fail we have to do this (including load) in a loop. Until it succeeds or max value is detected.
Some questions
- Is such solution correct?
- What memory ordering would be required to make it valid?
- Which
compare_exchangeshould be used?_weakor_strong? - Would it affect
release_referencefunction? - Is it used in practice?
fetch_addreturns previous value of the atomic. Would this not be sufficient to detect the overflow? - Igor Tandetnikabortjust as well if you detect the overflow after the fact, could you not? Either way, you likely cannot continue program execution safely. - Igor Tandetnikmemory_order_seq_cst. Otherwise you'll just shoot yourself in the foot. - 2501