🔒 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
- Once & OnceLock - Do something exactly one time
- Atomic Types - Super-safe number boxes
- Ordering for Atomics - Rules for reading and writing
- Send Trait - “Can I give this to another wizard?”
- 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
SeqCstuntil 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) - ✅
AtomicI32and 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
- When in doubt, use
SeqCst- It’s slower but always safe Arc<Mutex<T>>- Your best friend for shared mutable data- Trust the compiler - If it compiles, it’s thread-safe!
Once/OnceLock- Perfect for one-time initialization- 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! 🚀
