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
40: Threads in Rust
Up until now, everything we've written in this course has been sequential. One line follows another, and the CPU spends a lot of its time just waiting—waiting for a file to read, waiting for a network response, or waiting for a heavy calculation to finish. In the real world, that's a waste of resources. Today, we're going to look at how Rust handles concurrency using threads.
To make this concrete, let's build a simple "Site Health Checker." We have a list of URLs, and we want to check if they are reachable. Doing this one by one is painfully slow because the program hangs while waiting for the server to respond. We want to fire off a request for every site simultaneously.
Doing things one by one is too slow
First, let's look at the naive approach. I'll use a dummy function to simulate a network request so we don't have to pull in external crates like reqwest just for this lesson.
fn check_site(url: &str) {
println!("Checking {}...", url);
std::thread::sleep(std::time::Duration::from_secs(1)); // Simulate network lag
println!("{} is UP!", url);
}
fn main() {
let sites = vec!["google.com", "rust-lang.org", "github.com", "stackoverflow.com"];
for site in &sites {
check_site(site);
}
}
If you run this, it takes 4 seconds. That's unacceptable. We have four cores (or more) on our machine, and we're using exactly one of them while the others just sit there. Let's throw some threads at this.
Spawning our first workers
Rust makes it easy to spawn a thread using std::thread::spawn. It takes a closure containing the code you want to run in parallel. I'll wrap the check_site call in a thread for each URL.
use std::thread;
fn main() {
let sites = vec!["google.com", "rust-lang.org", "github.com", "stackoverflow.com"];
for site in &sites {
thread::spawn(|| {
check_site(site);
});
}
}
I just ran this, and it didn't work. In fact, it probably didn't print anything at all. Why? Because thread::spawn starts a thread in the background, but the main thread doesn't wait for them. The program hits the end of main and exits immediately, killing all the background threads before they even finish their first sleep cycle. We need to "join" these threads back to the main thread.
The borrow checker stops me in my tracks
Here is where I usually trip up when I'm rushing. I'll create a vector to hold the JoinHandles returned by spawn, and then I'll loop through them to call .join().
use std::thread;
fn main() {
let sites = vec!["google.com", "rust-lang.org", "github.com", "stackoverflow.com"];
let mut handles = vec![];
for site in &sites {
let handle = thread::spawn(|| {
check_site(site);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
If you try to compile this, Rust will scream at you. You'll see an error saying site does not live long enough. The compiler is worried that the main function might finish and drop the sites vector while a background thread is still trying to read a string from it. Rust's ownership rules are just as strict in threads as they are anywhere else: you cannot have a reference to data that might be destroyed.
Fixing it with ownership and Arc
There are two ways to fix this. We could clone() the string for each thread, but if we had a massive piece of data, cloning would be expensive. Instead, I'll use Arc (Atomic Reference Counted). Arc allows multiple threads to own a piece of data. When the last Arc is dropped, the data is finally cleaned up.
I'll wrap the sites list in an Arc and move a clone of that Arc into each thread. Note the move keyword before the closure—this tells Rust to move the ownership of the Arc clone into the thread's scope.
use std::thread;
use std::sync::Arc;
fn check_site(url: &str) {
println!("Checking {}...", url);
thread::sleep(std::time::Duration::from_secs(1));
println!("{} is UP!", url);
}
fn main() {
// Wrap our data in an Arc
let sites = Arc::new(vec!["google.com", "rust-lang.org", "github.com", "stackoverflow.com"]);
let mut handles = vec![];
for i in 0..sites.len() {
// Clone the Arc (not the data inside it)
let sites_ref = Arc::clone(&sites);
let handle = thread::spawn(move || {
// Use the index to get the site from the shared Arc
let site = &sites_ref[i];
check_site(site);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
Now it works. The total execution time drops from 4 seconds to roughly 1 second. We've effectively parallelized the I/O wait. I used a loop with an index here to keep it simple, but the core takeaway is the Arc: it's your primary tool for sharing read-only data across threads without fighting the borrow checker over lifetimes.
📋 Practical Task
Exercise: Building a Multi-threaded CPU-Intensive Calculator
In the previous example, we dealt with I/O wait. Now, let's deal with computation. Your task is to create a program that calculates the sum of squares for a large range of numbers, but splits the work across four different threads.
Requirements:
- Create a large range of numbers (e.g., 0 to 1,000,000).
- Divide this range into four equal chunks (e.g., 0-250k, 250k-500k, etc.).
- Spawn four threads, each calculating the sum of squares for its assigned chunk.
- Each thread should return its partial sum back to the main thread via the
JoinHandle. - The main thread must join all four threads and print the final combined total.
There are no comments for now.