C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
139: Memory Ordering in C11 Atomics
I've seen this exact bug crash production systems on ARM processors while working perfectly on x86 during development. It's one of the most insidious traps in C11 because the code looks logically sound, but it ignores how modern CPUs actually move data around.
#include <stdatomic.h>
#include <stdbool.h>
#include <stdio.h>
#include <pthread.h>
int shared_data = 0;
atomic_bool ready = false;
void* producer(void* arg) {
shared_data = 42; // Step 1: Prepare data
atomic_store_explicit(&ready, true, memory_order_relaxed); // Step 2: Signal ready
return NULL;
}
void* consumer(void* arg) {
while (!atomic_load_explicit(&ready, memory_order_relaxed)) {
// Spin until ready
}
printf("Data is: %d\n", shared_data); // Step 3: Read data
return NULL;
}
The Invisible Reordering Bug
At first glance, the logic is simple: the producer sets the data, then sets the flag. The consumer waits for the flag, then reads the data. If you run this on your Intel laptop, it will likely print "42" every single time. You'll think you're a genius. Then you deploy it to an ARM-based server or a mobile device, and suddenly it prints "0" occasionally.
The problem is memory_order_relaxed. When you tell the compiler and the CPU that an operation is "relaxed," you are giving them permission to reorder instructions as long as the behavior of that single atomic variable remains consistent. The CPU might decide that storing true to ready is faster to do before writing 42 to shared_data. Or, the consumer's CPU might speculatively read shared_data before it even checks if ready is true.
In the eyes of a relaxed memory model, there is no relationship between the atomic flag and the non-atomic data. They are two unrelated events.
Establishing a Happens-Before Relationship
To fix this, we need to create a synchronization point. We don't just need the variable to be atomic; we need to ensure that all memory writes performed by the producer before the flag is set are visible to the consumer after it sees the flag.
This is where memory_order_release and memory_order_acquire come in. Think of them as a pair of handcuffs that lock the ordering of memory operations.
void* producer(void* arg) {
shared_data = 42;
// "Release" ensures all prior writes in this thread are
// committed before this store becomes visible to others.
atomic_store_explicit(&ready, true, memory_order_release);
return NULL;
}
void* consumer(void* arg) {
// "Acquire" ensures that no subsequent reads in this thread
// happen before this load completes.
while (!atomic_load_explicit(&ready, memory_order_acquire)) {
// Spin
}
printf("Data is: %d\n", shared_data);
return NULL;
}
By changing the producer to release and the consumer to acquire, we establish a "happens-before" relationship. Everything the producer did before the release store is guaranteed to be visible to the consumer after the acquire load. I generally advise against using memory_order_seq_cst (sequentially consistent) for everything. While it's the safest default, it carries a heavy performance penalty on non-x86 architectures because it forces a global agreement across all cores. Acquire/Release is the "sweet spot" for most producer-consumer patterns.
📋 Practical Task
Implementing a Memory-Fence-Free Data Handover
You are tasked with creating a simple "Message Box" where one thread writes a large struct of data and another thread reads it. You must avoid using mutexes or memory_order_seq_cst to maximize performance.
Requirements:
- Define a struct
Payloadcontaining an array of 100 integers. - Use a global
Payloadinstance and anatomic_boolflag namedis_published. - Implement a
publish_datafunction that fills the payload with values (0-99) and then updates the flag using the correct memory order to ensure the data is visible. - Implement a
consume_datafunction that spins until the flag is set using the correct memory order, then verifies that the payload contains the correct values. - Run the code in a multi-threaded environment (using pthreads) and ensure there are no data races.
There are no comments for now.