memory_order enum that falls into this class: memory_order_relaxed.
This memory order is the most minimal guarantee that can be made. Recall from Atomics: What Is Memory Order? that the memory order across threads is undefined. All the relaxed memory order does is define this cross-thread ordering. An undefined memory order means partial operations can be visible across threads. A defined memory order removes this possibility. In a defined memory order, every thread observes all operations as having a specific sequential order, so that no truly parallel access occurs.
Let's use an example to flesh this out, taken from cpp reference:
// Thread 1:
r1 = atomic_load_explicit(y, memory_order_relaxed); // A
atomic_store_explicit(x, r1, memory_order_relaxed); // B
// Thread 2:
r2 = atomic_load_explicit(x, memory_order_relaxed); // C
atomic_store_explicit(y, 42, memory_order_relaxed); // D
Thread 1 by default has a memory order imposed on all of its own actions, so A->B is always what it will see. Likewise, thread 2 must see C->D. However, when we talk about how thread 1 observes each of these 4 operations, the only guarantee we have is that there is a coherent ordering. Aside from that, all of the 10 possible permutations may be observed by thread 1: A->B->C->D, A->B->D->C, A->C->B->D, A->D->B->D, A->C->D->B, A->D->C->B, C->A->B->D, C->A->D->B, D->A->B->C, D->A->C->B. In each case, A always occurs before B. That's the only rule.
On top of this, thread 2 has its own set of 10 possible permutations which it may observe. And the two threads are allowed to observe completely different orderings!
This means thread 1 may observe A->B->C->D, in which case y will be 42 and x will be 0, and thread 2 may observe C->D->A->B, making both x and y 42.
An undefined memory order is absolute chaos, but this hardly seems much better. Sure, I don't have any read/write tearing, but that's it basically. You might wonder when you could ever make use of a relaxed memory order. The most prominent example is when multiple threads are incrementing a counter, which is a common pattern to assign unique identifiers to the threads:
#include <stdio.h>
#include <stdint.h>
#include <pthread.h>
#include <stdatomic.h>
static atomic_uint nextId = 0;
static void *thread_main(void *arg) {
uint32_t id = atomic_fetch_add_explicit(&nextId, 1, memory_order_relaxed);
printf("Thread #%u\n", id);
return NULL;
}
int main(int argc, const char **argv) {
pthread_t thread1, thread2, thread3, thread4;
pthread_create(&thread1, NULL, thread_main, NULL);
pthread_create(&thread2, NULL, thread_main, NULL);
pthread_create(&thread3, NULL, thread_main, NULL);
pthread_create(&thread4, NULL, thread_main, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_join(thread3, NULL);
pthread_join(thread4, NULL);
return 0;
}
The only guarantee we need on line 10 is atomicity. It doesn't matter what order the threads grab their id in, because the atomicity guarantee tells us the 4 attempts will happen in a single sequential order. Each thread will read a unique current value and increment it. Memory order doesn't matter at all.
If we place this code in a file named relax.c, compile and run it, everything looks good:
$ gcc -Wall -o relaxed relaxed.c -lpthread
$ ./relaxed
Thread #0
Thread #2
Thread #3
Thread #1
Relaxed memory order is the weakest ordering. All atomics guarantee this. That's why atomics can claim to be atomic operations that are free of data races.