Is struct assignment atomic in C/C++?
I am writing a program which has one process reading and writing to a shared memory and another process only reading it. In the shared memory there is a struct like this:
struct A{
int a;
int b;
double c;
};
what I expect is to read the struct at once because while I am reading, the oth开发者_Go百科er process might be modifying the content of the struct. This can be achieved if the struct assignment is atomic, that is not interrupted. Like this:
struct A r = shared_struct;
So, is struct assignment atomic in C/C++? I tried searching the web but cannot find helpful answers. Can anyone help? Thank you.
No, both C and C++ standard don't guarantee assignment operations to be atomic. You need some implementation-specific stuff for that - either something in the compiler or in the operating system.
C and C++ support atomic types in their current standards.
C++11 introduced support for atomic types. Likewise C11 introduced atomics.
Do you need to atomically snapshot all the struct members? Or do you just need shared read/write access to separate members separately? The latter is a lot easier, see below.
C11 stdatomic and C++11 std::atomic provide syntax for atomic objects of arbitrary sizes. But if they're larger than 8B or 16B, they won't be lock-free on typical systems, though. (i.e. atomic load, store, exchange or CAS will be implemented by taking a hidden lock and then copying the whole struct).
If you just want a couple members, it's probably better to use a lock yourself and then access the members, instead of getting the compiler to copy the whole struct. (Current compilers aren't good at optimizing weird uses of atomics like that).
Or add a level of indirection, so there's a pointer which can easily be updated atomically to point to another struct
with a different set of values. This is the building block for RCU (Read-Copy-Update) See also https://lwn.net/Articles/262464/. There are good library implementations of RCU, so use one instead of rolling your own unless your use-case is a lot simpler than the general case. Figuring out when to free old copies of the struct is one of the hard parts, because you can't do that until the last reader thread is done with it. And the whole point of RCU is to make the read path as light-weight as possible...
Your struct is 16 bytes on most systems; just barely small enough that x86-64 can load or store the entire things somewhat more efficiently than just taking a lock. (But only with lock cmpxchg16b
). Still, it's not totally silly to use C/C++ atomics for this
common to both C++11 and C11:
struct A{
int a;
int b;
double c;
};
In C11 use the _Atomic
type qualifier to make an atomic type. It's a qualifier like const
or volatile
, so you can use it on just about anything.
#include <stdatomic.h>
_Atomic struct A shared_struct;
// atomically take a snapshot of the shared state and do something
double read_shared (void) {
struct A tmp = shared_struct; // defaults to memory_order_seq_cst
// or tmp = atomic_load_explicit(&shared_struct, memory_order_relaxed);
//int t = shared_struct.a; // UNDEFINED BEHAVIOUR
// then do whatever you want with the copy, it's a normal struct
if (tmp.a > tmp.b)
tmp.c = -tmp.c;
return tmp.c;
}
// or take tmp by value or pointer as a function arg
// static inline
void update_shared(int a, int b, double c) {
struct A tmp = {a, b, c};
//shared_struct = tmp;
// If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
atomic_store_explicit(&shared_struct, tmp, memory_order_relaxed);
}
Note that accessing a single member of an _Atomic
struct is undefined behaviour. It won't respect locking, and might not be atomic. So don't do int i = shared_state.a;
(C++11 don't compile that, but C11 will).
In C++11, it's nearly the same: use the std::atomic<T>
template.
#include <atomic>
std::atomic<A> shared_struct;
// atomically take a snapshot of the shared state and do something
double read_shared (void) {
A tmp = shared_struct; // defaults to memory_order_seq_cst
// or A tmp = shared_struct.load(std::memory_order_relaxed);
// or tmp = std::atomic_load_explicit(&shared_struct, std::memory_order_relaxed);
//int t = shared_struct.a; // won't compile: no operator.() overload
// then do whatever you want with the copy, it's a normal struct
if (tmp.a > tmp.b)
tmp.c = -tmp.c;
return tmp.c;
}
void update_shared(int a, int b, double c) {
struct A tmp{a, b, c};
//shared_struct = tmp;
// If you just need atomicity, not ordering, relaxed is much faster for small lock-free objects (no memory barrier)
shared_struct.store(tmp, std::memory_order_relaxed);
}
See it on the Godbolt compiler explorer
Of if you don't need to snapshot the entire struct, but instead just want each member to be separately atomic, then you can simply make each member an atomic type. (Like atomic_int
and _Atomic double
or std::atomic<double>
).
struct Amembers {
atomic_int a, b;
#ifdef __cplusplus
std::atomic<double> c;
#else
_Atomic double c;
#endif
} shared_state;
// If these members are used separately, put them in separate cache lines
// instead of in the same struct to avoid false sharing cache-line ping pong.
(Note that C11 stdatomic is not guaranteed to be compatible with C++11 std::atomic, so don't expect to be able to access the same struct from C or C++.)
In C++11, struct-assignment for a struct with atomic members won't compile, because std::atomic
deletes its copy-constructor. (You're supposed to load std::atomic<T> shared
into T tmp
, like in the whole-struct example above.)
In C11, struct-assignment for a non-atomic struct with atomic members will compile but is not atomic. The C11 standard doesn't specifically point this out anywhere. The best I can find is: n1570: 6.5.16.1 Simple assignment:
In simple assignment (=), the value of the right operand is converted to the type of the assignment expression and replaces the value stored in the object designated by the left operand.
Since this doesn't say anything about special handling of atomic members, it must be assumed that it's like a memcpy
of the object representations. (Except that it's allowed to not update the padding.)
In practice, it's easy to get gcc to generate asm for structs with an atomic member where it copies non-atomically. Especially with a large atomic member which is atomic but not lock-free.
No it isn't.
That is actually a property of the CPU architecture in relation to the memory layout of struck
You could use the 'atomic pointer swap' solution, which can be made atomic, and could be used in a lockfree scenario.
Be sure to mark the respective shared pointer (variables) as volatile if it is important that changes are seen by other threads 'immediately'This, in real life (TM) is not enough to guarantee correct treatment by the compiler. Instead program against atomic primitives/intrinsics directly when you want to have lockfree semantics. (see comments and linked articles for background)
Of course, inversely, you'll have to make sure you take a deep copy at the relevant times in order to do processing on the reading side of this.
Now all of this quickly becomes highly complex in relation to memory management and I suggest you scrutinize your design and ask yourself seriously whether all the (perceived?) performance benefits justify the risk. Why don't you opt for a simple (reader/writer) lock, or get your hands on a fancy shared pointer implementation that is threadsafe ?
精彩评论