Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Release-Consume ordering for reference counting

Consider the following simple reference counting functions (to be used with boost::intrusive_ptr):

class Foo {
    // ...

    std::atomic<std::size_t> refCount_{0};

    friend void intrusive_ptr_add_ref(Foo* ptr)
    {
        ++ptr->refCount_;  // ❶
    }

    friend void intrusive_ptr_release(Foo* ptr)
    {
        if (--ptr->refCount_ == 0) {  // ❷
            delete ptr;
        }
    }
};

I'm still learning memory ordering, and I'm wondering if the default memory ordering for fetch_add/sub (memory_order_seq_cst) is too strict in this case. Since the only ordering I want to ensure is between the ❶ and ❷, I think we can replace ❶ with

ptr->refCount_.fetch_add(1, std::memory_order_release);

and ❷ with

if (ptr->refCount_.fetch_sub(1, std::memory_order_consume) == 1) {

But memory ordering is still new and subtle to me, so I'm not sure if this will work correctly. Did I miss anything?

like image 728
Zizheng Tai Avatar asked Oct 22 '25 03:10

Zizheng Tai


2 Answers

Consulting the libc++ implementation of std::shared_ptr, you might want memory_order_relaxed for increment and memory_order_acq_rel for the decrement. Rationalizing this usage:

If the number increases, then all that matters is its consistency. The current thread is already sure that it's greater than zero. Other threads are unsynchronized so they will see the update at an indeterminate time before the next atomic modification, and perhaps at a time inconsistent with updates of other variables.

If the number decreases, then you need to be sure that the current thread has already finished modifying it. All updates from other threads must be visible. The current decrement must be visible to the next one. Otherwise, if the counter raced ahead of the object it was guarding, the object could be destroyed prematurely.

Cppreference has a nice page on memory ordering. It includes this note:

The specification of release-consume ordering is being revised, and the use of memory_order_consume is temporarily discouraged.

It also insinuates that no current compilers or CPUs implement consume; it's effectively the same as acquire.

like image 58
Potatoswatter Avatar answered Oct 23 '25 16:10

Potatoswatter


Incrementing the reference count does not require any synchronization in a correct program, just atomicity.

We pretend that references are owned by threads. A thread may only use a referenced object if the reference counter is at least one, and is guaranteed not to drop to zero while the object is being used, which either means that the thread has incremented the reference counter during its use of the object, or there is another mechanism that ensures this condition is met.

Thus, we assume that the thread incrementing the reference count owns the reference that ensures that it may access the object's reference counter, so no other thread may decrement the reference counter to zero while it is trying to increment the counter. The only thread allowed to drop the initial reference is either the current thread (after incrementing the reference count), or another thread once the current thread has signaled that its shared use of the object (i.e. the "ownership" of the original reference) has ceased -- both of these are visible effects.

On the other hand, decrementing the reference counter requires acquire and release semantics, as the object may be destroyed afterwards.

The CPP Reference's page on std::memory_order says

Typical use for relaxed memory ordering is incrementing counters, such as the reference counters of std::shared_ptr, since this only requires atomicity, but not ordering or synchronization (note that decrementing the shared_ptr counters requires acquire-release synchronization with the destructor).

like image 45
Simon Richter Avatar answered Oct 23 '25 17:10

Simon Richter