Skip to content

Rust Concurrency — Threads, Channels & Mutex

DodaTech Updated 2026-06-21 7 min read

Rust concurrency provides memory-safe threading through ownership and type system guarantees, preventing data races at compile time without a garbage collector or runtime overhead.

What You'll Learn

In this tutorial, you'll learn how Rust handles concurrency: creating threads with std::thread, message passing with channels, shared state with Mutex, the Send and Sync traits, and writing concurrent code that is guaranteed free of data races.

Why It Matters

Concurrency bugs — data races, deadlocks, race conditions — are among the hardest to debug. Rust's type system catches data races at compile time, making it the only mainstream language that guarantees thread safety. This is transformative for multi-threaded systems like web servers, databases, and real-time scanners.

Real-World Use

Tokio's async runtime uses thread pools for work stealing. The Servo browser engine uses Rust concurrency for parallel layout rendering. Database connection pools use Arc<Mutex<T>> for shared state. Durga Antivirus Pro scans files across dozens of threads with zero data races, guaranteed by the compiler.

flowchart LR
    subgraph "Thread 1"
        P1[Producer] --> C[Channel]
    end
    subgraph "Thread 2"
        C -->|message| P2[Consumer]
    end
    subgraph "Shared State"
        M[Mutex] -->|lock| D[Data]
    end
    T1[Thread A] -->|lock| M
    T2[Thread B] -->|lock| M
â„šī¸ Info

Prerequisites: Rust Ownership, Closures, and Smart Pointers.

Spawning Threads

Rust's std::thread module provides OS threads.

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

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

    for i in 1..=3 {
        println!("Main thread: iteration {}", i);
        thread::sleep(Duration::from_millis(150));
    }

    handle.join().expect("Child thread panicked");
    println!("Both threads completed");
}

Expected output:

Main thread: iteration 1
Child thread: iteration 1
Child thread: iteration 2
Main thread: iteration 2
Child thread: iteration 3
Child thread: iteration 4
Main thread: iteration 3
Child thread: iteration 5
Both threads completed

Message Passing with Channels

Channels allow threads to communicate by sending messages, avoiding shared state.

use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    let tx1 = tx.clone();
    thread::spawn(move || {
        let values = vec!["scanning", "analyzing", "detecting", "cleaning"];
        for v in values {
            tx1.send(format!("Worker 1: {}", v)).unwrap();
            thread::sleep(Duration::from_millis(200));
        }
    });

    thread::spawn(move || {
        let values = vec!["loading", "parsing", "matching", "reporting"];
        for v in values {
            tx.send(format!("Worker 2: {}", v)).unwrap();
            thread::sleep(Duration::from_millis(150));
        }
    });

    for received in rx {
        println!("Got: {}", received);
    }
}

Expected output:

Got: Worker 2: loading
Got: Worker 1: scanning
Got: Worker 2: parsing
Got: Worker 2: matching
Got: Worker 1: analyzing
Got: Worker 1: detecting
Got: Worker 2: reporting
Got: Worker 1: cleaning

Shared State with Mutex

Mutex<T> provides mutual exclusion for shared data across threads.

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

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

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

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

    println!("Final count: {}", *counter.lock().unwrap());
}

Expected output:

Final count: 10

Send and Sync Traits

Send allows ownership transfer between threads. Sync allows shared access through references.

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

// Rc is NOT Send -- cannot be transferred across threads
// Arc IS Send and Sync -- safe for multi-threaded use
// Mutex IS Send and Sync -- provides interior mutability across threads

fn verify_send_sync<T: Send + Sync>(_: &T) {
    println!("Type is Send + Sync");
}

fn main() {
    let arc_data = Arc::new(Mutex::new(String::from("shared data")));
    verify_send_sync(&arc_data);

    let (tx, rx) = mpsc::channel::<String>();

    let data = Arc::clone(&arc_data);
    thread::spawn(move || {
        let mut val = data.lock().unwrap();
        val.push_str(" modified by thread");
        tx.send(val.clone()).unwrap();
    });

    let result = rx.recv().unwrap();
    println!("Received: {}", result);
    println!("Main: {}", *arc_data.lock().unwrap());
}

Expected output:

Type is Send + Sync
Received: shared data modified by thread
Main: shared data modified by thread

Scoped Threads

Scoped threads allow borrowing non-'static data safely.

use std::thread;

fn main() {
    let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    thread::scope(|s| {
        s.spawn(|| {
            let sum: i32 = numbers.iter().filter(|&&n| n % 2 == 0).sum();
            println!("Even sum: {}", sum);
        });
        s.spawn(|| {
            let sum: i32 = numbers.iter().filter(|&&n| n % 2 != 0).sum();
            println!("Odd sum: {}", sum);
        });
    });

    println!("All scoped threads completed, numbers: {:?}", numbers);
}

Expected output:

Even sum: 30
Odd sum: 25
All scoped threads completed, numbers: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Concurrency and Security

Rust's concurrency model is inherently security-safe:

  • No data races: The type system prevents unsynchronized concurrent mutation
  • No deadlocks via type system: While Rust cannot prevent all deadlocks, patterns like parking_lot::ReentrantMutex are explicit
  • Race condition reduction: Channels force message-based communication, reducing shared mutable state
  • Panic safety: A panicking thread does not corrupt shared state (poisoning enables recovery)

Durga Antivirus Pro's multi-threaded scan engine uses these guarantees to process thousands of files per second with zero data races.

Common Mistakes

1. Moving Non-Send Types Across Threads

Rc<T> is not Send. Use Arc<T> for thread-safe reference counting.

2. Holding a Mutex Lock Across await Points

In async code, holding a mutex across .await can cause deadlocks. Use tokio::sync::Mutex instead.

3. Forgetting to Join Threads

Dropping a JoinHandle detaches the thread. Always join() to ensure completion and catch panics.

4. Creating Too Many Threads

Each thread has a stack (default 2MB on Linux). Use a thread pool ( rayon) for CPU-bound tasks, not raw threads.

5. Deadlocking with Multiple Mutexes

Lock two mutexes in different order in different threads. Always establish a consistent lock ordering.

Practice Questions

1. What is the difference between Send and Sync? Send types can be transferred across threads. Sync types can be shared across threads through references. Most types are both, but Rc<T> is neither, and Mutex<T> is both.

2. What does a channel provide? A channel provides message passing between threads. The sender (tx) sends values, the receiver (rx) receives them. Channels are multiple-producer, single-consumer by default.

3. How does Mutex ensure safety? Mutex<T> provides interior mutability across threads. It blocks threads until the lock is acquired, ensuring only one thread accesses the data at a time. The lock is automatically released when the guard goes out of scope.

4. What is thread poisoning? When a thread panics while holding a Mutex lock, the mutex becomes poisoned. Subsequent lock() calls return Err(PoisonError). This prevents accessing potentially inconsistent data.

5. Challenge: Build a concurrent file word counter that reads files in parallel using threads and channels, then aggregates results.

Mini Project: Concurrent Log Processor

use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;
use std::time::Instant;

fn process_log_line(line: &str, error_count: &mut usize, warning_count: &mut usize) {
    if line.contains("ERROR") {
        *error_count += 1;
    } else if line.contains("WARN") {
        *warning_count += 1;
    }
}

fn worker(id: usize, rx: mpsc::Receiver<String>, errors: Arc<Mutex<usize>>, warnings: Arc<Mutex<usize>>) {
    for line in rx {
        let mut e = errors.lock().unwrap();
        let mut w = warnings.lock().unwrap();
        process_log_line(&line, &mut e, &mut w);
    }
    println!("Worker {} finished", id);
}

fn main() {
    let log_data = vec![
        "INFO: System started",
        "ERROR: Connection timeout",
        "WARN: Disk space low",
        "INFO: User logged in",
        "ERROR: Database connection failed",
        "WARN: Memory usage high",
    ];

    let errors = Arc::new(Mutex::new(0usize));
    let warnings = Arc::new(Mutex::new(0usize));
    let (tx, rx) = mpsc::channel();
    let rx2 = rx;

    let e1 = Arc::clone(&errors);
    let w1 = Arc::clone(&warnings);
    let handle = thread::spawn(move || worker(1, rx2, e1, w1));

    for line in log_data {
        tx.send(line.to_string()).unwrap();
    }
    drop(tx);

    handle.join().unwrap();
    println!("Errors: {}, Warnings: {}", *errors.lock().unwrap(), *warnings.lock().unwrap());
}

FAQ

What is fearless concurrency?

Fearless concurrency is Rust's promise that the compiler guarantees your concurrent code is free of data races. If it compiles, it does not have unsynchronized concurrent access to mutable data.

Should I use channels or Mutex for shared state?

Use channels when data flows in one direction (producer-consumer). Use Mutex when multiple threads need arbitrary access to shared state. Channels are easier to reason about; Mutex is more flexible.

What is the difference between std::sync::Mutex and tokio::sync::Mutex?

std::sync::Mutex blocks the OS thread when locking. tokio::sync::Mutex yields the async task when locking, making it safe to hold across .await points in async code.

Smart Pointers
Async/Await
Unsafe Rust

What's Next

Explore Rust Async/Await for asynchronous I/O, and Unsafe Rust for raw thread primitives.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro