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
155: Tokio Sync Primitives: Mutex, RwLock, Semaphore
Why can't I just use the standard library Mutex in an async function?
This is probably the most common point of confusion when people move from synchronous Rust to Tokio. On the surface, std::sync::Mutex works. It compiles, and for tiny critical sections, it might even feel faster. But here is the danger: if you hold a std::sync::MutexGuard across an .await point, you are essentially hijacking the OS thread that the Tokio runtime is using to manage other tasks.
If your task is suspended at an .await while holding a standard mutex, no other task assigned to that thread can progress if they need that lock. Worse, you can end up with a deadlock that is incredibly hard to debug because the runtime's scheduler is now stuck. Tokio's Mutex is designed to be "async-aware." When you call .lock().await, if the lock is held, the task yields back to the executor so other work can get done. I always tell people: if the lock needs to be held while you're doing I/O or calling another async function, use tokio::sync::Mutex. If the lock is only held for a few nanoseconds to update a counter, the standard library one is actually fine—and faster.
use tokio::sync::Mutex;
use std::sync::Arc;
async fn update_shared_state(state: Arc<Mutex<Vec<String>>>, val: String) {
// This lock is held across an await point, which is exactly
// why we use the Tokio version here.
let mut guard = state.lock().await;
// Imagine this is a database call or a network request
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
guard.push(val);
}
When does an RwLock actually make sense over a Mutex?
I see a lot of developers reach for Mutex by default because it's simpler. But if you have a data structure that is read constantly but updated rarely—think of a configuration object or a routing table—a Mutex becomes a massive bottleneck. Every single reader has to wait in line, even though they aren't changing anything.
That's where RwLock (Read-Write Lock) comes in. It allows an unlimited number of concurrent readers, as long as no one is writing. The moment someone wants to write, they get exclusive access. I usually suggest benchmarking first, but as a rule of thumb: if your read-to-write ratio is higher than 10:1, RwLock will almost certainly give you a performance boost in a highly concurrent system.
use tokio::sync::RwLock;
use std::sync::Arc;
struct AppConfig {
api_key: String,
}
async fn handle_request(config: Arc<RwLock<AppConfig>>) {
// Multiple tasks can hold a read lock simultaneously
let conf = config.read().await;
println!("Using API key: {}", conf.api_key);
}
async fn rotate_key(config: Arc<RwLock<AppConfig>>, new_key: String) {
// This will wait until all active readers are finished
let mut conf = config.write().await;
conf.api_key = new_key;
}
How do I use a Semaphore to stop my app from crashing an external API?
Mutexes and RwLocks are about exclusive access to data. Semaphores are different; they are about concurrency limiting. I've had plenty of projects where we accidentally DDOSed our own internal microservices because we spawned 10,000 Tokio tasks that all tried to hit an HTTP endpoint at the exact same millisecond.
A Semaphore acts like a bowl of permits. To do work, a task must acquire a permit. If the bowl is empty, the task waits. Once the task is done, it drops the permit, putting it back in the bowl for the next task. It's the cleanest way to implement a "max concurrent requests" limit without building a complex queue system.
use tokio::sync::Semaphore;
use std::sync::Arc;
async fn fetch_url(url: String, semaphore: Arc<Semaphore>) {
// Acquire a permit before starting the request
let _permit = semaphore.acquire().await.unwrap();
println!("Fetching {}...", url);
// Simulate a network request
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
// Permit is automatically returned to the semaphore when _permit goes out of scope
}
#[tokio::main]
async fn main() {
// Only 3 concurrent requests allowed at any given time
let semaphore = Arc::new(Semaphore::new(3));
let mut handles = vec![];
for i in 0..10 {
let sem = Arc::clone(&semaphore);
handles.push(tokio::spawn(fetch_url(format!("https://api.example.com/{}", i), sem)));
}
for h in handles { h.await.unwrap(); }
}📋 Practical Task
Exercise: Building a Concurrent API Request Throttler
You are building a tool that processes a list of 50 IDs by fetching data for each from a remote API. However, the API has a strict limit: if you make more than 5 concurrent requests, it will return a 429 Too Many Requests error and ban your IP for an hour.
Your Task:
- Create a
Semaphorewith a capacity of 5. - Spawn 50 asynchronous tasks using
tokio::spawn. - Inside each task, use the semaphore to ensure that no more than 5 tasks are "fetching" (simulated by
tokio::time::sleep) at the same time. - Use an
Arc<Mutex<Vec<u32>>>to collect the IDs of the successfully processed requests. - Print the final list of processed IDs to verify all 50 were handled.
There are no comments for now.