Atomics and Thread Safety

Back

Loading concept...

🔒 Rust Concurrency: Atomics & Thread Safety

The Story of the Magical Library 📚

Imagine a magical library where many wizards work together at the same time. They all want to read and change books. But here’s the problem: if two wizards change the same book at the exact same moment, the book gets scrambled!

Rust gives us special tools to keep our “library” safe. Let’s learn them!


🎯 What You’ll Learn

  1. Once & OnceLock - Do something exactly one time
  2. Atomic Types - Super-safe number boxes
  3. Ordering for Atomics - Rules for reading and writing
  4. Send Trait - “Can I give this to another wizard?”
  5. Sync Trait - “Can wizards share this safely?”

1️⃣ Once & OnceLock: The “Only One Time” Spell

The Problem

Imagine you want to set up the library’s main desk. But 10 wizards all try to set it up at once! Chaos!

The Solution: Once

Once makes sure something happens exactly one time, even if 100 wizards try together.

use std::sync::Once;

static INIT: Once = Once::new();

fn setup_library() {
    INIT.call_once(|| {
        println!("Library is ready!");
    });
}

What happens:

  • First wizard calls setup_library() → “Library is ready!” prints
  • Second wizard calls it → Nothing happens (already done!)
  • Third wizard? Same thing. Nothing.

OnceLock: Store a Value Once

OnceLock is like Once, but it also stores a value.

use std::sync::OnceLock;

static CONFIG: OnceLock<String> = OnceLock::new();

fn get_config() -> &'static String {
    CONFIG.get_or_init(|| {
        String::from("Default Settings")
    })
}

Think of it like a treasure chest:

  • First person puts treasure in → Chest locks forever
  • Everyone else just reads what’s inside
graph TD A["Thread 1 calls get_or_init"] --> B{Is OnceLock empty?} B -->|Yes| C["Initialize value"] B -->|No| D["Return existing value"] C --> E["Lock forever"] E --> D

2️⃣ Atomic Types: Super-Safe Number Boxes 📦

Why Regular Numbers Are Dangerous

// ❌ DANGEROUS with multiple threads!
static mut COUNTER: i32 = 0;

If two threads change COUNTER at once, you get garbage!

Atomic = “All or Nothing”

Atomics are special numbers that change completely or not at all.

use std::sync::atomic::AtomicI32;
use std::sync::atomic::Ordering;

static COUNTER: AtomicI32 = AtomicI32::new(0);

fn add_one() {
    COUNTER.fetch_add(1, Ordering::SeqCst);
}

Common Atomic Types

Type What It Holds
AtomicBool true or false
AtomicI32 32-bit integer
AtomicI64 64-bit integer
AtomicUsize Pointer-sized number

Key Methods

let num = AtomicI32::new(5);

// Load: Read the value
let value = num.load(Ordering::SeqCst);

// Store: Write a value
num.store(10, Ordering::SeqCst);

// Fetch and add: Add and return old value
let old = num.fetch_add(3, Ordering::SeqCst);

// Compare and swap
num.compare_exchange(
    10,     // expected
    20,     // new value
    Ordering::SeqCst,
    Ordering::SeqCst
);

Think of compare_exchange like:

“IF the box has 10, THEN put 20. Otherwise, do nothing.”


3️⃣ Ordering: The Rules of Memory Magic 🎭

Why Do We Need Ordering?

Computers are sneaky! They rearrange your code to run faster. Usually that’s fine. But with multiple threads, it can cause chaos!

Ordering tells the computer: “Follow these rules!”

The 5 Orderings (Simple to Strong)

graph TD A["Relaxed"] --> B["Acquire"] A --> C["Release"] B --> D["AcqRel"] C --> D D --> E["SeqCst"] style A fill:#90EE90 style E fill:#FF6B6B
Ordering Speed Safety Use When
Relaxed ⚡⚡⚡ Low Just counting, nothing else depends on it
Acquire ⚡⚡ Medium Reading shared data
Release ⚡⚡ Medium Writing shared data
AcqRel High Read + Write together
SeqCst 🐢 Highest When in doubt, use this!

Simple Rule for Beginners

🎓 Just use SeqCst until you’re an expert!

It’s slower but always safe.

Example: Flag Pattern

use std::sync::atomic::{AtomicBool, Ordering};

static READY: AtomicBool = AtomicBool::new(false);
static DATA: AtomicI32 = AtomicI32::new(0);

// Writer thread
fn writer() {
    DATA.store(42, Ordering::Release);
    READY.store(true, Ordering::Release);
}

// Reader thread
fn reader() {
    while !READY.load(Ordering::Acquire) {}
    let value = DATA.load(Ordering::Acquire);
    // value is guaranteed to be 42!
}

4️⃣ Send Trait: “Can I Mail This?” 📬

The Big Question

“Can I safely give this thing to another thread?”

If yes → It implements Send!

Things That ARE Send

  • ✅ Numbers (i32, f64, etc.)
  • String
  • Vec<T> (if T is Send)
  • Arc<T> (if T is Send + Sync)

Things That Are NOT Send

  • Rc<T> - Not safe to share!
  • ❌ Raw pointers
  • MutexGuard (the lock itself)
use std::thread;

let data = vec![1, 2, 3];

// ✅ This works! Vec is Send
thread::spawn(move || {
    println!("{:?}", data);
});
use std::rc::Rc;

let data = Rc::new(42);

// ❌ ERROR! Rc is not Send
thread::spawn(move || {
    println!("{}", data);
});

The Compiler Protects You!

Rust automatically checks Send at compile time. You can’t accidentally share unsafe things!


5️⃣ Sync Trait: “Can We All Read This Together?” 👥

The Big Question

“Can multiple threads read this at the same time?”

If yes → It implements Sync!

The Magic Formula

T is Sync if &T is Send

Translation: “If I can safely share a reference, then it’s Sync.”

Things That ARE Sync

  • ✅ All primitive types
  • Mutex<T> (if T is Send)
  • RwLock<T> (if T is Send + Sync)
  • AtomicI32 and friends

Things That Are NOT Sync

  • Cell<T> - Interior mutability without locks
  • RefCell<T> - Same reason
  • Rc<T> - Reference counting isn’t atomic
graph TD A["Your Type T"] --> B{Is &T safe to share?} B -->|Yes| C["T is Sync ✅"] B -->|No| D["T is NOT Sync ❌"]

Example

use std::sync::Arc;
use std::thread;

// Arc + Mutex = Safe sharing!
let counter = Arc::new(Mutex::new(0));

let handles: Vec<_> = (0..10).map(|_| {
    let c = Arc::clone(&counter);
    thread::spawn(move || {
        let mut num = c.lock().unwrap();
        *num += 1;
    })
}).collect();

for h in handles {
    h.join().unwrap();
}

🎓 Quick Reference Table

Concept Purpose Example
Once Run code once Initialize logger
OnceLock Store value once Global config
AtomicI32 Safe shared number Counters
Ordering Memory sync rules SeqCst for safety
Send Safe to transfer Most things!
Sync Safe to share refs Thread-safe types

🌟 The Golden Rules

  1. When in doubt, use SeqCst - It’s slower but always safe
  2. Arc<Mutex<T>> - Your best friend for shared mutable data
  3. Trust the compiler - If it compiles, it’s thread-safe!
  4. Once/OnceLock - Perfect for one-time initialization
  5. Atomics - Great for simple counters and flags

🎉 You Did It!

You now understand:

  • How to initialize things exactly once
  • How to use atomic numbers safely
  • What ordering means and when to use each
  • The difference between Send and Sync

Go build amazing concurrent programs! 🚀

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.