In this lesson, you will learn how to synchronize state changes across threads by using atomic operations directly on raw memory locations.
The Problem of Visibility
When you work with raw pointers, you are effectively bypassing Rust’s borrow checker. If one thread writes to a memory address while another thread reads from it without synchronization, you trigger a data race—which is undefined behavior. Even if you use volatile operations, they only prevent compiler optimizations; they do not guarantee that other CPU cores see your updates in a coherent order.
Atomic operations are the solution. They allow you to perform read-modify-write operations as a single, indivisible unit of work, ensuring that no other thread can observe an intermediate, half-written state.
Synchronizing via Atomic Orderings
When performing atomic operations on a raw pointer (typically via core::sync::atomic), you must specify a memory ordering. This dictates how the hardware and compiler handle the "happens-before" relationship:
- Relaxed: Only guarantees atomicity of the operation, not the order of surrounding memory accesses.
- Acquire/Release: Creates a synchronization point. A
Releasestore ensures that all memory writes performed by the thread before the store are visible to any thread that performs anAcquireload on the same address. - SeqCst: The strongest guarantee; it provides a global, consistent ordering of all operations.
Here is how you might perform an atomic operation on a raw memory address:
use core::sync::atomic::{AtomicI32, Ordering};
// Assuming 'ptr' is a valid raw pointer to an i32
let atomic_ptr = ptr as *const AtomicI32;
unsafe {
// Atomically increment the value at the raw pointer
(*atomic_ptr).fetch_add(1, Ordering::SeqCst);
}
By using Ordering::SeqCst, you ensure that this increment is globally visible and correctly synchronized with other atomic operations. Without these orderings, the CPU might reorder instructions, leading to situations where a thread sees a "new" value for one variable but a "stale" value for another, breaking your program's logic.
Key Takeaway: Atomic operations on raw memory provide the foundation for thread-safe synchronization by enforcing hardware-level visibility guarantees that prevent data races.