If you only need to be able to set enable = true
, then stdatomic.h
with release / acquire ordering gives you exactly what you're asking for. (In x86 asm, normal stores/loads have release/acquire semantics, so yes blocking compile-time reordering is sort of sufficient. But the right way to do that is with atomic
, not volatile
.)
But if you want to be able to set enable = false
to "lock out" readers again while you modify it, then you need a more complicated update pattern. Either re-invent a mutex manually with atomics (bad idea; use an standard library mutex instead of that), or do something that allows wait-free read-only access by multiple readers when no writer is in the middle of an update.
Either RCU a or seqlock could be good here.
For a seqlock, instead of an enable = true/false flag, you have a sequence number. A reader can detect a "torn" write by checking the sequence number before and then again after reading the other members. (But then all the member have to be atomic
, using at least mo_relaxed
, otherwise it's data race undefined behaviour just from reading them in C, even if you discard the value. You also need sufficient ordering on the loads that check the counter. e.g. probably acquire on the first one, then acquire on the shared_struct->b
load to make sure the 2nd load of the sequence number is ordered after it. (acquire
is only a one-way barrier: an acquire load after a relaxed load wouldn't give you what you need.)
RCU makes the readers always completely wait-free; they just dereference a pointer to the currently-valid struct. Updates are as simple as atomically replacing a pointer. Recycling old structs is where it gets complicated: you have to be sure every reader thread is done reading a block of memory before you reuse it.
Simply setting enable = false
before changing the other struct members doesn't stop a reader from seeing enable == true
and then seeing inconsistent / partially-updated values for the other members while a writer is modifying them. If you don't need to do that, but only ever release new objects for access by other threads, then the sequence you describe is fine with atomic_store_explicit(&foo->enable, true, memory_order_release)
.