Rust LogoAtomic Operations

Atomic operations are fundamental building blocks for concurrent programming, particularly when sharing mutable state across multiple threads without locks or mutexes. An operation is considered 'atomic' if it appears to happen instantaneously and indivisibly from the perspective of other threads. This means that either the entire operation completes, or none of it does, and no other thread can observe a partial state or interrupt the operation mid-way.

In concurrent systems, standard read, modify, and write operations (like `x = x + 1`) are not atomic. They typically involve loading the value into a register, incrementing it, and then storing it back. If multiple threads perform this sequence concurrently, a data race can occur, leading to lost updates or incorrect results. Atomic operations solve this problem by providing guarantees that the entire read-modify-write sequence (or other single operations like load/store) will execute without interference from other threads.

Rust provides atomic types in the `std::sync::atomic` module. These types are specifically designed for thread-safe access to primitive integer types, booleans, and pointers. Key atomic types include:
* `AtomicBool`
* `AtomicI8`, `AtomicI16`, `AtomicI32`, `AtomicI64`, `AtomicI128`
* `AtomicU8`, `AtomicU16`, `AtomicU32`, `AtomicU64`, `AtomicU128`
* `AtomicIsize`, `AtomicUsize`
* `AtomicPtr<T>`

These types expose methods like:
* `load(ordering)`: Reads the current value.
* `store(val, ordering)`: Writes a new value.
* `swap(val, ordering)`: Atomically replaces the current value with `val` and returns the old value.
* `compare_exchange(current, new, success_ordering, failure_ordering)`: Atomically compares the current value with `current`. If they are equal, the value is updated to `new` and `Ok(old_value)` is returned. Otherwise, `Err(current_value)` is returned.
* `fetch_add(val, ordering)`: Atomically adds `val` to the current value and returns the old value.
* `fetch_sub(val, ordering)`: Atomically subtracts `val` from the current value and returns the old value.
* `fetch_and(val, ordering)`, `fetch_nand(val, ordering)`, `fetch_or(val, ordering)`, `fetch_xor(val, ordering)`: Bitwise atomic operations.

The `ordering` parameter is crucial for controlling memory visibility and synchronization between threads. It dictates how the compiler and CPU reorder instructions around the atomic operation and how changes become visible to other threads. Common orderings include:
* `Relaxed`: Provides no memory ordering guarantees beyond atomicity of the operation itself. Other memory operations can be reordered around it.
* `Acquire`: Ensures that all memory operations *after* this acquire operation in program order are visible *before* it to any thread that performs a `Release` operation on the same atomic variable.
* `Release`: Ensures that all memory operations *before* this release operation in program order are visible *after* it to any thread that performs an `Acquire` operation on the same atomic variable.
* `AcqRel`: Combines `Acquire` and `Release` semantics, suitable for read-modify-write operations.
* `SeqCst` (Sequentially Consistent): Provides the strongest memory ordering. All `SeqCst` operations appear to execute in a single global order across all threads, preventing any reordering. This comes with a higher performance cost but is the easiest to reason about for beginners.

Atomic operations are a powerful tool for building high-performance, lock-free data structures and algorithms, but their correct usage, especially with complex memory orderings, requires a deep understanding of concurrent programming principles.

Example Code

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Instant;

fn main() {
    // Create an atomic counter, wrapped in Arc to be shared across threads.
    let counter = Arc::new(AtomicU64::new(0));
    let num_threads = 8;
    let increments_per_thread = 1_000_000;

    println!("Starting atomic counter example with {} threads, each incrementing {} times.", num_threads, increments_per_thread);

    let start_time = Instant::now();

    let mut handles = vec![];

    for i in 0..num_threads {
        let counter_clone = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            for _ in 0..increments_per_thread {
                // Atomically increment the counter.
                // Ordering::Relaxed is sufficient here because we only care about
                // the final count, not the ordering of side effects across threads.
                counter_clone.fetch_add(1, Ordering::Relaxed);
            }
            println!("Thread {} finished its increments.", i);
        });
        handles.push(handle);
    }

    // Wait for all threads to complete.
    for handle in handles {
        handle.join().unwrap();
    }

    let final_count = counter.load(Ordering::SeqCst); // Use SeqCst for final load to ensure all writes are visible
    let expected_count = num_threads * increments_per_thread;

    let duration = start_time.elapsed();

    println!("\nAll threads finished.");
    println!("Final count: {}", final_count);
    println!("Expected count: {}", expected_count);
    println!("Counter matches expected: {}", final_count == expected_count);
    println!("Time taken: {:?}", duration);

    // Example of compare_exchange
    let data = Arc::new(AtomicU64::new(10));
    let data_clone = Arc::clone(&data);
    let cas_handle = thread::spawn(move || {
        // Try to change 10 to 20. If successful, print it.
        match data_clone.compare_exchange(10, 20, Ordering::AcqRel, Ordering::Relaxed) {
            Ok(old_val) => println!("CAS: Successfully changed value from {} to 20.", old_val),
            Err(current_val) => println!("CAS: Failed to change value. Current value is {}.", current_val),
        }
    });
    cas_handle.join().unwrap();
    println!("Value after CAS attempt: {}", data.load(Ordering::SeqCst));
}