Skip to main content

Command Palette

Search for a command to run...

Rust Concurrency: Threads, Channels, and Safe Parallelism

Updated
โ€ข6 min readโ€ขView as Markdown
Rust Concurrency: Threads, Channels, and Safe Parallelism
A

Olamide is my name. I am a blockchain/Frontend developer with experience building smart contracts on both EVM (Solidity) and non-EVM compatible blockchains.

๐Ÿ˜„ Pronouns: He/Him. ๐Ÿ“ซ How to reach me on handles

I'm a full-stack blockchain developer crafting the next generation of blockchain experiences with ReactJS and Solidity. I'm not just a developer; I'm a trailblazer, obsessed with pushing the boundaries of what's possible in this transformative space.

I'm not just about code, I'm about impact. I've built smart contracts that shatter the status quo, empower users, and scale without compromise. My playground? EVM-compatible blockchains and L2 solutions, where I ensure trust, security, and groundbreaking performance into every line of code.

Introduction

Concurrency in most languages is terrifying. You spin up threads, share memory, and hope nobody modifies the same data simultaneously. Race conditions, deadlocks, and subtle bugs lurk in every corner.

Rust makes concurrency safe. The compiler prevents data races. You can't share mutable data unsafely. Rust forces you to think about what happens when multiple threads access the same data.

In this guide, you'll learn:

  • How Rust threads work

  • Message passing with channels

  • Shared mutable state safely

  • Common concurrency patterns

  • Deadlock prevention

  • Real-world concurrent systems

By the end, you'll write concurrent Rust confidently.

Concept Overview

Concurrency vs Parallelism

Concurrency: Multiple tasks make progress (might run on same core). Parallelism: Multiple tasks run simultaneously (multiple cores).

Rust handles both. Async is concurrency on one thread. Threads are parallelism across cores.

Thread Safety in Rust

Most languages require runtime checks for thread safety. Rust's type system enforces it at compile time.

Two key traits:

  • Send: Safe to send to another thread

  • Sync: Safe to share references across threads

Rust automatically implements these for types that follow the rules. You can't accidentally share mutable data.

Technical Explanation

Spawning Threads

use std::thread;

let handle = thread::spawn(|| {
    println!("Hello from thread!");
});

handle.join().unwrap();  // Wait for thread to finish

Channels for Message Passing

Sender and Receiver:

let (tx, rx) = mpsc::channel();  // Multi-producer, single-consumer

thread::spawn(move || {
    tx.send("Hello").unwrap();
});

println!("{}", rx.recv().unwrap());

Shared Mutable State

Arc + Mutex:

use std::sync::{Arc, Mutex};

let counter = Arc::new(Mutex::new(0));

let c = Arc::clone(&counter);
thread::spawn(move || {
    let mut num = c.lock().unwrap();
    *num += 1;
});

Code Examples

Example 1: Basic Thread

use std::thread;
use std::time::Duration;

fn main() {
    thread::spawn(|| {
        for i in 1..5 {
            println!("Thread: {}", i);
            thread::sleep(Duration::from_millis(100));
        }
    });
    
    for i in 1..3 {
        println!("Main: {}", i);
        thread::sleep(Duration::from_millis(100));
    }
}

Explanation: Threads run concurrently with main.

Expected Behavior: Interleaved output from both.

Example 2: Thread with join()

let handle = thread::spawn(|| {
    for i in 1..5 {
        println!("{}", i);
        thread::sleep(Duration::from_millis(100));
    }
});

handle.join().unwrap();
println!("Done");

Explanation: join() waits for thread to complete.

Expected Behavior: Waits for thread, then prints "Done".

Example 3: Channels for Communication

use std::sync::mpsc;
use std::thread;

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    tx.send("Hello").unwrap();
    tx.send("from").unwrap();
    tx.send("thread").unwrap();
});

for msg in rx {
    println!("{}", msg);
}

Explanation: Send messages between threads.

Expected Behavior: Prints "Hello", "from", "thread".

Example 4: Shared Mutable State

use std::sync::{Arc, Mutex};
use std::thread;

let counter = Arc::new(Mutex::new(0));

let mut handles = vec![];

for _ in 0..5 {
    let c = Arc::clone(&counter);
    handles.push(thread::spawn(move || {
        let mut num = c.lock().unwrap();
        *num += 1;
    }));
}

for handle in handles {
    handle.join().unwrap();
}

println!("Counter: {}", *counter.lock().unwrap());  // 5

Explanation: Multiple threads safely modify shared counter.

Expected Behavior: Prints "Counter: 5".

Example 5: Thread Pool Pattern

use std::sync::{Arc, Mutex, mpsc};
use std::thread;

struct ThreadPool {
    workers: Vec<Worker>,
    sender: mpsc::Sender<Message>,
}

impl ThreadPool {
    fn new(size: usize) -> ThreadPool {
        let (sender, receiver) = mpsc::channel();
        let receiver = Arc::new(Mutex::new(receiver));
        
        let workers = (0..size)
            .map(|_| Worker::new(Arc::clone(&receiver)))
            .collect();
        
        ThreadPool { workers, sender }
    }
    
    fn execute<F>(&self, f: F)
    where
        F: FnOnce() + Send + 'static,
    {
        let msg = Message::NewJob(Box::new(f));
        self.sender.send(msg).unwrap();
    }
}

Explanation: Thread pool reuses threads for jobs.

Best Practice: Thread pools are more efficient than creating threads per task.

Common Errors

Error 1: Move Data Without Arc

let data = vec![1, 2, 3];

thread::spawn(|| {
    println!("{:?}", data);  // โœ— ERROR - data doesn't live long enough
});

How to Fix: Use Arc:

let data = Arc::new(vec![1, 2, 3]);
let data_clone = Arc::clone(&data);

thread::spawn(move || {
    println!("{:?}", data_clone);
});

Error 2: Mutable Reference Across Threads

let mut data = 5;
thread::spawn(|| {
    data = 10;  // โœ— ERROR - can't borrow mutably
});

How to Fix: Use Mutex:

let data = Arc::new(Mutex::new(5));
let d = Arc::clone(&data);

thread::spawn(move || {
    *d.lock().unwrap() = 10;
});

Error 3: Deadlock

// Two threads lock in opposite order
// Thread 1: lock A, then B
// Thread 2: lock B, then A
// Deadlock!

How to Fix: Always lock in the same order.

Performance & Memory Insights

Thread Overhead

Each thread has ~2MB stack overhead. Async tasks are much lighter.

Lock Contention

Mutex locks serialize access. High contention = performance bottleneck. Consider RwLock or lock-free structures.

Real-World Use Cases

Use Case 1: Worker Pool

let pool = ThreadPool::new(4);
for job in jobs {
    pool.execute(job);
}

Use Case 2: Producer-Consumer

let (tx, rx) = mpsc::channel();

thread::spawn(move || {
    for item in producer() {
        tx.send(item).unwrap();
    }
});

for item in rx {
    process(item);
}

Use Case 3: Parallel Data Processing

let data: Vec<_> = data
    .into_par_iter()  // rayon crate
    .map(|x| expensive_computation(x))
    .collect();

Best Practices

1. Use Channels for Message Passing

// โœ“ Good - channels prevent data race
let (tx, rx) = mpsc::channel();
thread::spawn(move || { tx.send(data).unwrap(); });

// โœ— Avoid - shared mutable state
let data = Arc::new(Mutex::new(data));

2. Lock as Short as Possible

// โœ“ Good - lock scope minimal
{
    let mut data = counter.lock().unwrap();
    *data += 1;
}  // Lock released

// โœ— Bad - unnecessary lock holding
let mut data = counter.lock().unwrap();
expensive_operation();
*data += 1;

3. Use Thread Pools

// โœ“ Efficient
let pool = ThreadPool::new(4);

// โœ— Wasteful - create thread per task
for task in tasks {
    thread::spawn(|| run(task));
}

Beginner Mistakes

Mistake 1: Forgetting to join()

// โœ— Thread might not finish
thread::spawn(|| println!("hello"));

// โœ“ Wait for completion
let h = thread::spawn(|| println!("hello"));
h.join().unwrap();

Mistake 2: Unnecessary Locks

// โœ— Over-locking
let data = Arc::new(Mutex::new(vec![1, 2, 3]));

// โœ“ Use Arc only for shared ownership
let data = vec![1, 2, 3];  // One thread owns it

Conclusion

Rust's concurrency guarantees are powerful. Data races are impossible. Deadlocks are preventable.

Key Takeaways:

  • Threads run parallel code

  • Channels pass messages between threads

  • Arc + Mutex for shared mutable state

  • Send and Sync traits enforce thread safety

Next Steps:

  • Build a multi-threaded web server

  • Experiment with thread pools

  • Learn about RwLock for reader-heavy workloads

  • Study the Rayon crate for data parallelism

Rust's concurrency safety is one of its greatest strengths. Master it, and you'll build systems others only dream of.