Skip to main content

How to write concurrent Rust with threads and channels

Write concurrent Rust: spawn threads, move data in, pass messages over mpsc channels, share state with Arc and Mutex, and rely on Send and Sync for compile-time data-race safety.

Difficulty
Intermediate
Duration
50 minutes
Steps
6

Fearless concurrency in Rust

Rust calls its concurrency story "fearless" because the ownership and type system catch data races at compile time. If code that shares data unsafely between threads compiles, it is, by design, free of data races. You coordinate threads with channels for message passing or with Arc and Mutex for shared state.

Prerequisites

  • Rust and Cargo installed
  • Familiarity with ownership and borrowing

Steps

1. Spawn a thread

use std::thread;

let handle = thread::spawn(|| {
    println!("running in a thread");
});
handle.join().unwrap();

2. Move data into a thread

A thread may outlive the spawning scope, so it must own the data it uses. The move keyword transfers ownership.

let data = vec![1, 2, 3];
thread::spawn(move || {
    println!("{:?}", data);
});

3. Send messages with channels

The standard library provides multi-producer, single-consumer channels.

use std::sync::mpsc;

let (tx, rx) = mpsc::channel();
thread::spawn(move || tx.send(42).unwrap());
let value = rx.recv().unwrap(); // 42

4. Share state with Arc and Mutex

To share mutable state across threads, wrap it in a Mutex for safe access and an Arc for shared ownership.

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

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

5. Understand Send and Sync

The Send and Sync marker traits tell the compiler which types can cross thread boundaries. The compiler enforces them automatically, which is what makes the safety guarantees hold.

6. Join threads and handle results

Call join on each handle to wait for completion and to propagate panics or return values.

Verification

Write a program that spawns several threads, each incrementing a shared Arc<Mutex<i32>>, join them all, and print the final count. Confirm the total is correct. Then try sharing a non-thread-safe type and confirm the compiler refuses to build it.

Next Steps

Explore scoped threads, RwLock for read-heavy workloads, atomic types for lock-free counters, and async runtimes such as Tokio for I/O-bound concurrency.

Prerequisites

  • Basic Rust and ownership
  • Rust and Cargo installed

Steps

  • 1
    Spawn a thread
  • 2
    Move data into a thread
  • 3
    Send messages with channels
  • 4
    Share state with Arc and Mutex
  • 5
    Understand Send and Sync
  • 6
    Join threads and handle results