In C++20, we got the capability to sleep on atomic variables, waiting for their value to change.
We do so by using the std::atomic::wait method.
Unfortunately, while wait has been standardized, wait_for and wait_until are not. Meaning that we cannot sleep on an atomic variable with a timeout.
Sleeping on an atomic variable is anyway implemented behind the scenes with WaitOnAddress on Windows and the futex system call on Linux.
Working around the above problem (no way to sleep on an atomic variable with a timeout), I could pass the memory address of an std::atomic to WaitOnAddress on Windows and it will (kinda) work with no UB, as the function gets void* as a parameter, and it's valid to cast std::atomic<type> to void*
On Linux, it is unclear whether it's ok to mix std::atomic with futex. futex gets either a uint32_t* or a int32_t* (depending which manual you read), and casting std::atomic<u/int> to u/int* is UB. On the other hand, the manual says
The uaddr argument points to the futex word. On all platforms, futexes are four-byte integers that must be aligned on a four- byte boundary. The operation to perform on the futex is specified in the futex_op argument; val is a value whose meaning and purpose depends on futex_op.
Hinting that alignas(4) std::atomic<int> should work, and it doesn't matter which integer type is it is as long as the type has the size of 4 bytes and the alignment of 4.
Also, I have seen many places where this trick of combining atomics and futexes is implemented, including boost and TBB.
So what is the best way to sleep on an atomic variable with a timeout in a non UB way? Do we have to implement our own atomic class with OS primitives to achieve it correctly?
(Solutions like mixing atomics and condition variables exist, but sub-optimal)
You shouldn't necessarily have to implement a full custom
atomicAPI, it should actually be safe to simply pull out a pointer to the underlying data from theatomic<T>and pass it to the system.Since
std::atomicdoes not offer some equivalent ofnative_handlelike other synchronization primitives offer, you're going to be stuck doing some implementation-specific hacks to try to get it to interface with the native API.For the most part, it's reasonably safe to assume that first member of these types in implementations will be the same as the
Ttype -- at least for integral values [1]. This is an assurance that will make it possible to extract out this value.This isn't actually the case.
std::atomicis guaranteed by the standard to be Standard-Layout Type. One helpful but often esoteric properties of standard layout types is that it is safe toreinterpret_castaTto a value or reference of the first sub-object (e.g. the first member of thestd::atomic).As long as we can guarantee that the
std::atomic<u/int>contains only theu/intas a member (or at least, as its first member), then it's completely safe to extract out the type in this manner:This approach should also hold on windows to cast the
atomicto the underlying type before passing it to thevoid*API.Note: Passing a
T*pointer to avoid*that gets reinterpreted as aU*(such as anatomic<T>*tovoid*when it expects aT*) is undefined behavior -- even with standard-layout guarantees (as far as I'm aware). It will still likely work because the compiler can't see into the system APIs -- but that doesn't make the code well-formed.Note 2: I can't speak on the
WaitOnAddressAPI as I haven't actually used this -- but any atomics API that depends on the address of a properly aligned integral value (void*or otherwise) should work properly by extracting out a pointer to the underlying value.[1] Since this is tagged
C++20, you can verify this withstd::is_layout_compatiblewith astatic_assert:(Thanks to @apmccartney for this suggestion in the comments).
I can confirm that this will be layout compatible for Microsoft's STL, libc++, and libstdc++; however if you don't have access to
is_layout_compatibleand you're using a different system, you might want to check your compiler's headers to ensure this assumption holds.