I have the following code:
#include <atomic>
int main () {
std::atomic<uint32_t> value(0);
value.fetch_add(1, std::memory_order::relaxed);
static_assert(std::atomic<uint32_t>::is_always_lock_free);
return 0;
}
It compiles and so it means std::atomic<uint32_t>::is_always_lock_free
is true.
Then, the assembly code looks like this with gcc 10 and -std=c 20 -O3 -mtune=skylake-avx512 -march=skylake-avx512
:
0000000000401050 <main>:
401050: c7 44 24 fc 00 00 00 mov DWORD PTR [rsp-0x4],0x0
401057: 00
401058: f0 ff 44 24 fc lock inc DWORD PTR [rsp-0x4]
40105d: 31 c0 xor eax,eax
40105f: c3 ret
Many posts point out that read-modify-write operation (fetch_add()
here) can't be an atomic operation without a lock.
My question is what std::atomic::is_always_lock_free
being true
really means.
This page states Equals true if this atomic type is always lock-free and false if it is never or sometimes lock-free.
What does it mean by "this atomic type is always lock-free" then?
CodePudding user response:
"Lock" here is in the sense of "mutex", not specifically in reference to the x86 instruction prefix named lock
.
A trivial and generic way to implement std::atomic<T>
for arbitrary types T
would be as a class containing a T
member together with a std::mutex
, which is locked and unlocked around every operation on the object (load, store, exchange, fetch_add, etc). Those operations can then be done in any old way, and need not use atomic machine instructions, because the lock protects them. This implementation would be not lock free.
A downside of such an implementation, besides being slow in general, is that if two threads try to operate on the object at the same time, one of them will have to wait for the lock, which may actually block and cause it to be scheduled out for a while. Or, if a thread gets scheduled out while holding the lock, every other thread that wants to operate on the object will have to wait for the first thread to get scheduled back in and complete its work first.
So it is desirable if the machine supports truly atomic operations on T
: a single instruction or sequence that other threads cannot interfere with, and which will not block other threads if interrupted (or perhaps cannot be interrupted at all). If for some type T
the library has been able to specialize std::atomic<T>
with such an implementation, then that is what we mean by saying it is lock free. (It is just confusing on x86 because the atomic instructions used for such implementations are named lock
. On other architectures they might be called something else, e.g. ARM64's ldxr/stxr
exclusive load/store instructions.)
The C standard allows for types to be "sometimes lock free": maybe it is not known at compile time whether std::atomic<T>
will be lock-free, because it depends on special machine features that will be detected at runtime. It's even possible that some objects of type std::atomic<T>
are lock-free and others are not. That's why atomic_is_lock_free
is a function and not a constant. It checks whether this particular object is lock-free on this particular day.
However, it might be the case for some implementations that certain types can be guaranteed, at compile time, to always be lock free. That's what is_always_lock_free
is used to indicate, and note that it's a constexpr bool
instead of a function.