June 26, 2025
Understanding Threads, Processes, and When to Reach for Concurrency
Threads are a fundamental concept in modern software engineering, but knowing when to use them is crucial. Let’s break down what threads are, why they matter, and when they’re the right tool for the job—using clear analogies, technical explanations, and Rust code examples to illustrate the concepts.
Think of your computer as a busy restaurant:
Threads are “lightweight” because they share the same memory space (kitchen) as their parent process, making communication fast but also introducing challenges—like not bumping into each other or mixing up ingredients.
graph TD
Program["Program (Recipe Book)"] -->|Start| Process["Process (Chef)"]
Process -->|Spawns| Thread1["Thread (Sous-chef 1)"]
Process -->|Spawns| Thread2["Thread (Sous-chef 2)"]
Process -->|Spawns| Thread3["Thread (Sous-chef 3)"]
Process -.->|Owns| Memory["Memory (Ingredients)"]
Thread1 -.-> Memory
Thread2 -.-> Memory
Thread3 -.-> Memory
Threads can be tricky:
That’s why we avoid threads unless they’re truly needed. But sometimes, they’re the best solution.
Threads shine in three main scenarios:
Let’s explore each with analogies, kernel behavior, and Rust examples.
Imagine a chef waiting for bread to bake. If the chef stands idle, the kitchen’s productivity drops. Instead, a sous-chef (thread) can wait for the oven, while the main chef keeps cooking.
graph LR
MainThread["Main Thread"] -- "Spawns" --> WorkerThread["Worker Thread"]
WorkerThread -- "Performs Blocking I/O" --> IO["Disk/Network I/O"]
MainThread -- "Continues Processing" --> OtherWork["Other Work"]
When a process performs I/O (like reading from disk), the kernel may block it—pausing execution until the operation completes. This is called blocking I/O.
use std::thread;
use std::fs::File;
use std::io::Read;
fn main() {
let handle = thread::spawn(|| {
let mut file = File::open("data.txt").unwrap();
let mut contents = String::new();
file.read_to_string(&mut contents).unwrap();
println!("File contents: {}", contents);
});
// Main thread can do other work here
handle.join().unwrap();
}
If you use asynchronous I/O (like Rust’s tokio
with async
/await
), you might not need threads at all.
Suppose a chef needs to knead dough for 20 minutes. Rather than tying up the main chef, a sous-chef can handle the kneading, letting the main chef prepare other dishes.
graph TD
MainThread["Main Thread (Core 1)"]
WorkerThread["Worker Thread (Core 2)"]
MainThread -- "Runs Main Logic" --> MainTask["Main Task"]
WorkerThread -- "Runs Heavy Computation" --> HeavyTask["Heavy Task"]
CPU-bound tasks consume lots of processor time. If left on the main thread, they can starve other operations.
use std::thread;
fn heavy_computation() -> u64 {
(0..1_000_000_000).sum()
}
fn main() {
let handle = thread::spawn(|| {
let result = heavy_computation();
println!("Computation result: {}", result);
});
// Main thread can handle other tasks
handle.join().unwrap();
}
If a restaurant suddenly gets hundreds of orders, one chef can’t keep up. Multiple sous-chefs (threads) can each handle a few orders, increasing throughput.
graph TD
Listener["Listener Thread"] -- "Accepts" --> Conn1["Connection 1"]
Listener -- "Accepts" --> Conn2["Connection 2"]
Listener -- "Accepts" --> ConnN["Connection N"]
Conn1 -- "Handled by" --> Worker1["Worker Thread 1"]
Conn2 -- "Handled by" --> Worker2["Worker Thread 2"]
ConnN -- "Handled by" --> WorkerN["Worker Thread N"]
When you have many small, independent tasks (like handling network connections), a single thread can become a bottleneck. Spawning multiple threads allows you to process more tasks in parallel.
use std::thread;
fn handle_connection(id: usize) {
println!("Handling connection {}", id);
}
fn main() {
let mut handles = vec![];
for i in 0..10 {
handles.push(thread::spawn(move || {
handle_connection(i);
}));
}
for handle in handles {
handle.join().unwrap();
}
}
Threads are powerful, but come with trade-offs. Use them when:
Always weigh the complexity and risks of threading against the benefits. Sometimes, asynchronous programming or process-based concurrency is a better fit.
Happy coding! And remember: with great concurrency comes great responsibility.