Date: Sat, 15 Feb 2025 18:49:04 -0700
> On Feb 15, 2025, at 17:38, Brian Bi <bbi5291_at_[hidden]> wrote:
>
>
> It is puzzling that all the standard library implementations have the acquire fence but the standard doesn't seem to require it. Maybe it does, and we all just failed to find it.
Well, the typical implementation of shared_ptr is something like
template<class T>
shared_ptr {
// ...
struct control_block {
T *obj;
atomic<long> refcount;
};
control_block *cb;
};
And not only the managed object, but also the control block, must be freed when the reference counter reaches 0. And it seems clear that shared_ptr does have to avoid data races on freeing the control block, which requires there to be some amount of ordering. So it's natural to have an implementation of ~shared_ptr<T> or reset() that does something like
if (cb->refcount.fetch_sub(1, memory_order_release) == 1) {
atomic_thread_fence(memory_order_acquire);
delete cb->obj;
delete cb;
}
Or simply acq_rel ordering on the fetch_sub. But the concern is that someone could theoretically try
if (cb->refcount.fetch_sub(1, memory_order_release) == 1) {
delete cb->obj;
atomic_thread_fence(memory_order_acquire);
delete cb;
}
> I'd expect it to say something like this: if D is the invocation of the destructor that invokes the deleter, and D' is any other invocation of the destructor of a `shared_ptr` that shares ownership with it, then D' strongly happens before the invocation of the deleter.
I'd like to see that too.
>
>
> It is puzzling that all the standard library implementations have the acquire fence but the standard doesn't seem to require it. Maybe it does, and we all just failed to find it.
Well, the typical implementation of shared_ptr is something like
template<class T>
shared_ptr {
// ...
struct control_block {
T *obj;
atomic<long> refcount;
};
control_block *cb;
};
And not only the managed object, but also the control block, must be freed when the reference counter reaches 0. And it seems clear that shared_ptr does have to avoid data races on freeing the control block, which requires there to be some amount of ordering. So it's natural to have an implementation of ~shared_ptr<T> or reset() that does something like
if (cb->refcount.fetch_sub(1, memory_order_release) == 1) {
atomic_thread_fence(memory_order_acquire);
delete cb->obj;
delete cb;
}
Or simply acq_rel ordering on the fetch_sub. But the concern is that someone could theoretically try
if (cb->refcount.fetch_sub(1, memory_order_release) == 1) {
delete cb->obj;
atomic_thread_fence(memory_order_acquire);
delete cb;
}
> I'd expect it to say something like this: if D is the invocation of the destructor that invokes the deleter, and D' is any other invocation of the destructor of a `shared_ptr` that shares ownership with it, then D' strongly happens before the invocation of the deleter.
I'd like to see that too.
Received on 2025-02-16 01:49:22