Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
115: Atomic Types in std::sync::atomic
Up until now, whenever we've needed to share a piece of mutable state across threads, we've reached for a Mutex or a RwLock. For most things, that's the right call. But there are times when using a Mutex feels like using a sledgehammer to crack a nut. I'm talking about those tiny pieces of state—a counter, a boolean flag, or a status code—where the overhead of locking and unlocking a mutex actually costs more than the operation itself.
The overhead of the safe bet
Imagine we're building a high-throughput web server and we want to keep track of the total number of requests processed across all worker threads. The "safe" way we've learned is to wrap a usize in an Arc<Mutex<usize>>. It looks like this:
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
let mut num = counter.lock().unwrap();
*num += 1;
}
}));
}
This works perfectly, but it's inefficient. Every single time a thread wants to increment that number, it has to request a lock, potentially put the thread to sleep if another thread holds it, and then unlock it. We're spending more time managing the lock than actually adding one to a number. If this is in the hot path of your application, you're creating a massive bottleneck where every thread is just waiting in line to touch one integer.
Leaning on the hardware
This is where std::sync::atomic comes in. Atomic types—like AtomicUsize, AtomicBool, and AtomicI32—don't use locks. Instead, they leverage special instructions built directly into your CPU. When you tell an AtomicUsize to increment, the CPU handles the "read-modify-write" cycle as a single, indivisible operation. No other thread can sneak in halfway through.
Here is how I'd rewrite that request counter using atomics:
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
let counter = Arc::new(AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(thread::spawn(move || {
for _ in 0..1000 {
counter.fetch_add(1, Ordering::SeqCst);
}
}));
}
Notice we didn't call .lock(). We used fetch_add. This is significantly faster because it avoids the OS kernel's involvement in thread synchronization. We're talking about a direct conversation between the code and the hardware.
The trade-off: Memory Ordering
You probably noticed that weird Ordering::SeqCst argument. This is the "price" you pay for leaving the safety of a Mutex. Because modern CPUs and compilers love to reorder instructions to squeeze out performance, you have to explicitly tell Rust how strict the synchronization needs to be.
- SeqCst (Sequentially Consistent): This is the gold standard. It guarantees that all threads see all operations in the exact same order. It's the safest choice and the one I recommend using until you have a very specific reason not to.
- Relaxed: This is the fastest. It only guarantees that the operation itself is atomic, but it makes no promises about the order in which other memory operations are seen. It's great for counters where you don't care if Thread A sees the update a few nanoseconds after Thread B.
- Acquire/Release: These are the middle ground, used for building custom locks or signaling mechanisms between threads.
I'll be honest: memory ordering is one of the deepest rabbit holes in computer science. Don't feel like you need to master the formal memory model today. For 99% of use cases, SeqCst is what you want. If you're optimizing for every single CPU cycle and move to Relaxed, just be aware that you're stepping into a world where "weird" bugs can happen on different CPU architectures (like ARM vs x86).
📋 Practical Task
Implementing a Concurrent Active-Connection Tracker
You are simulating a server that tracks how many clients are currently connected. Because clients connect and disconnect constantly across many threads, using a Mutex would be too slow.
Your Task:
- Create a struct called
ConnectionTrackerthat holds anAtomicUsize. - Implement a method
increment_connections(&self)that increases the count by 1. - Implement a method
decrement_connections(&self)that decreases the count by 1. - Implement a method
get_count(&self) -> usizethat returns the current number of connections. - In your
mainfunction, spawn 5 threads. Each thread should simulate 100 clients connecting and then disconnecting (increment then decrement). - Finally, print the total count. If implemented correctly, the final count should be 0.
There are no comments for now.