Alternatives to std::atomic_ref
Asked Answered
M

2

5

I'm looking for an alternative to std::atomic_ref which might be used without needing C++20 support. I've considered casting pointers to std::atomic but that does not seem like a safe option.

The use-case would be to apply atomic operations to non-atomic objects for the lifetime of the atomic reference to avoid race. The objects accessed cannot all be made atomic, so a wrapper like the atomic_ref would be needed.

Any help is appreciated!

Mattah answered 20/5, 2021 at 12:57 Comment(9)
Can you provide example usage? Just because an object is accessed via an atomic_ref doesn't make concurrent access safe. I fail to see the safety you are after.Catalase
@Catalase Avoiding data race? I think the question is pretty clear.Nomography
@Catalase I was under the impression that while the atomic reference object exists, the object that it references is considered to be an atomic object, therefore, concurrent reads and writes are well defined. My example usage is basically that I want to be able to atomically store some value to a non-atomic object while concurrent reads should also see valid states.Mattah
@DK While atomic refs to an object exist, the variable can be used atomically through those refs. It is not legal to access the object directly as long as an atomic ref to it exists.Scully
@Scully yes, sorry, that is what I had in mindMattah
If you can't make it atomic, you can't make std::atomic_ref out of it either.Odoacer
@n.'pronouns'm. I don't think it's true. You can't, for instance, create a vector of atomic ints. But you can create a vector of ints and then work with its elements atomically through atomic_ref.Nomography
@DanielLangr Good point, but you can wrap an atomic int in a custom class and make a vector of those.Odoacer
@n.'pronouns'm. You can, but using atomic_ref is much simpler and, I believe, understandable.Nomography
T
4

In GCC/clang (and other compilers that implement GNU C extensions), you can use the __atomic builtins, such as

int load_result = __atomic_load_n(&plain_int_var, __ATOMIC_ACQUIRE);

That's how atomic_ref<T> is implemented on such compilers: just wrappers for those builtins. (That's why atomic_ref is super light-weight, and it's normally best to construct one for free every time you need one, not keep around a single atomic_ref.)

Since you won't have std::atomic_ref<T>::required_alignment, it's normally sufficient to give the objects natural alignment, i.e. alignas( sizeof(T) ) T foo; to make sure __atomic operations are actually atomic, as well as having memory-order guarantees. (On many implementations, all plain T that support lock-free atomics at all already get sufficient alignment, but for example some 32-bit systems only align int64_t by 4 bytes, but 8-byte atomics are only atomic with 8-byte alignment. x86 gcc -m32 had a problem with this in C++ for a while, and for a lot longer with _Atomic in C, finally fixed in 2020, although it only affected struct members.)


reinterpret_cast< std::atomic<T>* > may actually work in practice on most compilers, maybe not even being UB depending on the internals of atomic<>.

(Most?) other compilers implement atomic (and atomic_ref) in a way that's similar to GNU C, I think, using builtin functions. e.g. for MSVC, something like _InterlockedExchange() to implement atomic<>::exchange.

In mainstream C++ implementations, atomic<T> has the same size and layout as a plain T. (The size is something you can static_assert) It's in theory possible for a non-lock-free atomic<> to include a mutex or something, but normal implementations don't (Where is the lock for a std::atomic?). (Partly for compat with C11 _Atomic, which IIRC has some requirements about even uninitialized or maybe zero-initialized objects still working properly. But also just for size reasons.)

Despite ISO C++ not guaranteeing that it's well-defined, you will basically end up calling __atomic_fetch_add_n or InterlockedAdd on an int member var of atomic<int> with the same address as your original plain int.

That might still technically be UB; there's a rule about structs being compatible up to the first difference in their definition, but I'm less sure about an int* into a struct or especially a struct{int;}* pointer to an int object. I think that violates the strict-aliasing rule.

But I think still unlikely to break in practice. Still, the possible breakage would only show up under optimization, and be dependent on surrounding code, meaning it's not something you can easily write a unit-test for.

However, the most likely-to-break scenario would be if the same function (after inlining) was reading or writing the plain variable mixed with operations on the same variable through an atomic<>* or atomic<>& reference. Especially if there isn't any kind of memory barrier separating those accesses, such as calling some_thread.join(). If you mixing atomic and non-atomic access within one function (after inlining), this may be safe and portable enough to work until you can use atomic_ref<> properly.


The other good short-term option is manually using either GNU C or MSVC atomic builtins directly, if your source code currently only cares about one or the other. Or roll your own (limited subset of) atomic_ref using the versions of these functions you actually need.

Thunderous answered 20/5, 2021 at 15:10 Comment(1)
Thanks a lot for the comprehensive answer! I’ll make sure to look into these.Mattah
N
2

If your compiler supports OpenMP (most of them do), you can mark your object access with #pragma atomic. Possibly with a proper operation (read, write, update, capture) and memory-ordering semantics.

EDIT

Alternatively, it also seems that Boost provides atomic_ref available to pre-C++20 codes: https://www.boost.org/doc/libs/1_75_0/doc/html/atomic/interface.html#atomic.interface.interface_atomic_ref

Another way might be casting a non-atomic object into an atomic one by using reinterpret_cast. This solution will likely cause undefined behavior, but may actually work with some implementations. It is, for instance, used in the Facebook Folly library: https://github.com/facebook/folly/blob/master/folly/synchronization/PicoSpinLock.h#L95.

Nomography answered 20/5, 2021 at 13:9 Comment(5)
Thanks, I will look into this.Mattah
@DK You're welcome. I added some updates, which you might be interested in.Nomography
Perfect, these are very useful!Mattah
@DanielLangr: Interesting that some code in the wild uses casts to atomic<T>. The only way I could see that breaking would be strict-aliasing violations, the way normal implementations work. (As long as you make sure the objects are sufficiently aligned, of course! atomic<T> may need more alignment than plain T; especially 64-bit objects on 32-bit systems may be under-aligned.)Thunderous
@PeterCordes I agree. In theory, atomic<T> may even have larger binary representation size (multiple member variables), but with mainstream implementations on mainstream architectures these casts will likely work.Nomography

© 2022 - 2024 — McMap. All rights reserved.