Consider the following code:
struct T { std::atomic<int> a = 2; };
T* t = new T();
// Thread 1
if(t->a.fetch_sub(1,std::memory_order_relaxed) == 1)
delete t;
// Thread 2
if(t->a.fetch_sub(1,std::memory_order_relaxed) == 1)
delete t;
We know exactly one of Thread 1 and Thread 2 will execute the delete
. But are we safe? I mean suppose Thread 1 will execute the delete
. Is it guaranteed that when Thread 1 started the delete
, Thread 2 won't even read t
?
let call operation t->a.fetch_sub(1,std::memory_order_relaxed)
Release
Release
is atomic modification of a
Release
occur in a total orderThread 1
do Release
first and than Thread 2
do Release
after itThread 1
view value 2 and because 2 != 1 just exit and not
access t anymoreThread 2
view value 1 and because 1 == 1 call delete t
note that call delete
happens after Release
in Thread 2
and
Release
in Thread 2
happens after Release
in Thread 1
so call delete
in Thread 2
happens after Release
in Thread 1
which not access t anymore after Release
but in real life (not in this concrete example) in general we need use memory_order_acq_rel
instead memory_order_relaxed
.
this is because the real objects usual have more data fields, not only atomic reference count.
and threads can write/modify some data in object. from another side - inside destructor we need view all modifications made by other threads.
because this every not last Release must have memory_order_release
semantic. and last Release
must have memory_order_acquire
for view after this all modification . let some example
#include <atomic>
struct T {
std::atomic<int> a;
char* p;
void Release() {
if(a.fetch_sub(1,std::memory_order_acq_rel) == 1) delete this;
}
T()
{
a = 2, p = nullptr;
}
~T()
{
if (p) delete [] p;
}
};
// thread 1 execute
void fn_1(T* t)
{
t->p = new char[16];
t->Release();
}
// thread 2 execute
void fn_2(T* t)
{
t->Release();
}
in destructor ~T()
we must view result of t->p = new char[16];
even if destructor will be called in thread 2. if use memory_order_relaxed
formal this is not guaranteed.
but with memory_order_acq_rel
thread after final Release
, which will be executed with memory_order_acquire
semantic too (because memory_order_acq_rel
include it) will be view result of t->p = new char[16];
operation because it happens before another atomic operation on the same a
variable with memory_order_release
semantic (because memory_order_acq_rel
include it)
because still exist doubt, i try make yet bit another prove
given:
struct T {
std::atomic<int> a;
T(int N) : a(N) {}
void Release() {
if (a.fetch_sub(1,std::memory_order_relaxed) == 1) delete this;
}
};
question: are code will be correct and T will be deleted ?
let N = 1
- so a == 1
at begin and Release()
called one time.
here exist question ? somebody say that this is "UB" ? (a
accessed after delete this
begin execute or how ?!)
delete this
can not begin execute until a.fetch_sub(1,std::memory_order_relaxed)
will be calculated, because delete this
depended from result of a.fetch_sub
. compiler or cpu can not reorder delete this
before a.fetch_sub(1,std::memory_order_relaxed)
finished.
because a == 1
- a.fetch_sub(1,std::memory_order_relaxed)
return 1, 1 == 1
so delete this
will be called.
and all access to object before delete this
begin execute.
so code correct and T
deleted in case N == 1
.
let now in case N == n
all correct. so look for case N = n + 1. (n = 1,2..∞)
a.fetch_sub
is modifications of atomic variable.a.fetch_sub
will be executed first (in
order of modification a)a.fetch_sub
return
n + 1 != 1 (n = 1..∞)
- so Release()
in which will be executed this
first a.fetch_sub
, exit without call delete this
delete this
yet not called - it will be called only
after a.fetch_sub
which return 1, but this a.fetch_sub
will be called after first a.fetch_sub
a == n
after first a.fetch_sub
finished (this
will be before all other n
a.fetch_sub
)Release
(where first a.fetch_sub
executed ) exit
without delete this
and it finish access object before delete this
startn
rest Release()
calls and a == n
before any
a.fetch_sub
, but this case already OKone more note for those who think that code not safe / UB.
not safe can be only if we begin delete before any access of object finished.
but delete will be only after a.fetch_sub
return 1.
this mean that another a.fetch_sub
already modify a
because a.fetch_sub
is atomic - if we view it side effect (modification of a
) - a.fetch_sub
- no more access a
really if operation write value to memory location (a
) and after this access this memory again - this already not atomic by sense.
so if we view result of atomic modification - it already completed and no more access variable
as result delete will be already after all access to a
complete.
and here not need any special memory order (relaxed,acq,rel) for atomic. even relaxed order is ok. we need only atomicity of operation.
memory_order_acq_rel
need if object T containing not only a
counter. and we want in destructor view all memory modifications to another fields of T
This should be safe assuming each thread only runs once because t
wouldn't be deleted until both threads have already read the pointer. Although I would still strongly recommend the use of a std::shared_ptr
if you want to manage the lifetime of a pointer with reference counting instead of trying to do it yourself. That's what it was made for.
suppose Thread 1 will execute the
delete
. Is it guaranteed that when Thread 1 started thedelete
, Thread 2 won't even readt
?
Yes, in order for thread 1 to delete t
, the read in the second thread that decrements the value must have already occurred otherwise the if
statement would not have evaluated to true and t
would not have been deleted.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With