Rust track

Lesson 21 · Part 5: Cargo, Testing, Concurrency & Async

Concurrency

Spawning threads, message passing and shared-state synchronisation

Part 5: Cargo, Testing, Concurrency & Async5 min readFree lesson

Concurrency in Rust blocks data races—which were once called 'fearsome triggers'—at compile time. The secret is that the ownership and borrowing rules from chapters 6 to 9 apply directly to concurrency as well — since two threads cannot have mutable references to the same data simultaneously, data races are inherently prevented. In this chapter, we will learn how to create threads, use channels, and handle shared mutable state using only the standard library.

Learning Objectives

  • Create a new thread with std::thread::spawn and wait with join.
  • Send and receive messages safely between threads using mpsc::channel.
  • Handle shared mutable state with the Arc<Mutex<T>> pattern.
  • Understand how move closures affect captures.

Key Concepts

1) thread::spawn and JoinHandle

use std::thread;

let handle = thread::spawn(|| {
    //New thread body
});
handle.join().expect("Child thread panic");
  • spawn immediately starts a new thread and returns JoinHandle<T>.
  • JoinHandle::join() waits for the thread to finish and returns the thread's return value (or panic payload).

2) move Closures

If a closure uses outer variables, it is borrowed by default. However, a thread can live longer than its parent, so a borrowed reference can become unsafe. Therefore, we use the move keyword to move ownership to the closure.

let s = String::from("hi");
thread::spawn(move || { println!("{s}") }); //Ownership of s to the closure

3) mpsc::channel — Message Passing

"Multi-producer, single-consumer (mpsc)"

let (tx, rx) = mpsc::channel();
tx.send(value).expect("Receiver closed");
for msg in rx { /* ... */ }

The sender tx can be cloned to clone() and distributed across multiple threads, and when all senders are dropped, the receiver's iteration naturally ends.

4) Arc<Mutex<T>> — Shared Mutable State

Type Meaning
Arc<T> Atomic reference counting — multiple threads share the same value
Mutex<T> Lock that allows only one thread to access the data at a time
Arc<Mutex<T>> Combination of both — shared mutable
let counter = Arc::new(Mutex::new(0));
let c = Arc::clone(&counter);
thread::spawn(move || {
    let mut num = c.lock().unwrap();
    *num += 1;
});

lock() returns a LockResult<MutexGuard<T>>, and when MutexGuard goes out of scope, the lock is released (RAII).

5) Send and Sync

Two marker traits from the standard library express concurrency safety.

  • T: Send — ownership can be transferred to another thread.
  • T: Sync — multiple threads can hold &T at the same time.

Arc and Mutex satisfy these, so they can be safely shared. Rc is not Send, so compilation fails in multi-threading.

Examples

Example 1 — ex01_thread.rs: Thread + join

//Example 21-1: Creating and joining threads with std::thread
//
//thread::spawn starts a new thread and returns a JoinHandle.
//With JoinHandle::join(), you can wait for the thread to finish and receive the result.

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

fn main() {
    let handle = thread::spawn(|| {
        for i in 1..=3 {
            println!("[Child] Count {i}");
            thread::sleep(Duration::from_millis(20));
        }
        //The last expression becomes the join result
        "Child finished"
    });

    for i in 1..=3 {
        println!("[Main] Count {i}");
        thread::sleep(Duration::from_millis(20));
    }

    let result = handle.join().expect("Child thread panic");
    println!("Child join result: {result}");
}

Example 2 — ex02_channel.rs: Channel

//Example 2 in Chapter 21: Sending messages between threads with mpsc::channel
//
//This is a 'multi-producer, single-consumer' channel.
//Tx can be cloned, there is only one Rx.

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

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

    //Two producers
    let tx2 = tx.clone();
    thread::spawn(move || {
        for word in ["a1", "a2", "a3"] {
            tx.send(format!("[A] {word}")).expect("Channel closed");
        }
    });
    thread::spawn(move || {
        for word in ["b1", "b2", "b3"] {
            tx2.send(format!("[B] {word}")).expect("Channel closed");
        }
    });

    //Consumer — ends naturally when both senders are dropped
    for msg in rx {
        println!("Received: {msg}");
    }
}

Example 3 — ex03_arc_mutex.rs: Arc<Mutex>

//Example 21-3: Shared Mutable State with Arc<Mutex<T>>
//
//Arc — Atomically Reference Counted, allows multiple threads to share the same data.
//Mutex — Only one thread can acquire the lock and access the data at a time.

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

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

    for _ in 0..5 {
        //Arc::clone only increases the reference count (cheap)
        let c = Arc::clone(&counter);
        let h = thread::spawn(move || {
            //lock() returns LockResult — safe if no other thread has panicked
            let mut num = c.lock().expect("Mutex lock failed");
            *num += 1;
        });
        handles.push(h);
    }

    for h in handles {
        h.join().expect("Child thread panic");
    }

    println!("counter = {}", counter.lock().expect("Mutex lock failed"));
}

Common Mistakes

Q. If you remove the move keyword, it will not compile.

A. The closure borrows an outer value, and the compiler prevents it because the borrowing may live longer than the parent scope. It can be resolved by transferring ownership to the closure with move || { ... }.

Q. Can't I use Rc<RefCell<T>>?

A. It is possible in a single thread, but in multithreaded contexts the compiler prevents it because it's not Send/Sync. For multithreaded sharing, use Arc<Mutex<T>> (or RwLock).

Q. I heard lock().unwrap() can cause a panic?

A. If another thread panics while holding a lock, the mutex becomes "poisoned." Subsequent lock() calls return Err(PoisonError). At the beginner level, .expect() or .unwrap() is usually sufficient.

Q. How do I close a channel?

A. When all tx (senders) are dropped, the channel automatically closes and the receiver iteration ends. There is no method to close it explicitly.

Summary

  • The default pattern is thread::spawn + JoinHandle::join.
  • To move values to threads, use move closure.
  • Pass messages with mpsc::channel, share mutable state with Arc<Mutex<T>>.
  • Borrow rules + Send/Sync prevent data races at compile time.

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/concurrency/Cargo.tomlTOML
[package]
name = "concurrency"
version = "0.1.0"
edition = "2021"
default-run = "concurrency"

[dependencies]
src/concurrency/src/main.rsRust
fn main() {
    println!("This crate has three executable examples.");
    println!();
    println!("  cargo run --bin ex01_thread");
    println!("  cargo run --bin ex02_channel");
    println!("  cargo run --bin ex03_arc_mutex");
}

Assignment

Problem 1 — Summing 1..=100 with Four Threads

  • File name: homework_01/src/main.rs
  • Key concepts: thread::spawn, join, work division

Requirements

  • Divide the sum from 1 to 100 among four threads.
  • Thread 1: 1..=25, Thread 2: 26..=50, Thread 3: 51..=75, Thread 4: 76..=100
  • Each thread returns the sum of its segment (thread::spawn + final expression).
  • In main, gather the four results and print the total sum.

Expected output

Sum of 1..=100 = 5050

Hint

  • let h = thread::spawn(move || (start..=end).sum::<i32>());
  • Collect the four handles in a Vec and for h in handles { total += h.join().unwrap(); }.

Problem 2 — Collect and Print Words Received via Channel

  • File name: homework_02/src/main.rs
  • Key concepts: mpsc::channel, clone

Requirements

  • Two sending threads each send ["hello", "world"], ["from", "rust"] to the channel.
  • The main thread receives all messages, gathers them with Vec<String>, then prints the length.

Expected output

Number of words received: 4

Hint

  • let (tx, rx) = mpsc::channel::<String>();
  • The second sender is tx.clone().
  • let words: Vec<String> = rx.iter().collect();.

Check your answers

Try solving it yourself first, then compare your answer with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/Cargo.tomlTOML
[package]
name = "homework_01"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_01/src/main.rsRust
//Key Points:
//- Each thread calculates the sum of its own segment and then returns it as the final expression.
//- The result of JoinHandle::join() is a Result — unwrap to retrieve the value.

use std::thread;

fn main() {
    let ranges = [(1i32, 25), (26, 50), (51, 75), (76, 100)];
    let mut handles = vec![];
    for (start, end) in ranges {
        let h = thread::spawn(move || (start..=end).sum::<i32>());
        handles.push(h);
    }

    let mut total = 0;
    for h in handles {
        total += h.join().expect("Child thread panic");
    }

    println!("The total sum of 1..=100 = {total}");
}
homework_02/Cargo.tomlTOML
[package]
name = "homework_02"
version = "0.1.0"
edition = "2021"

[dependencies]
homework_02/src/main.rsRust
//Key Points:
//- You can clone() the sender tx so that multiple threads can send to the same channel.
//- When all senders are dropped, the receiver's iteration naturally ends.

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

fn main() {
    let (tx, rx) = mpsc::channel::<String>();
    let tx2 = tx.clone();

    thread::spawn(move || {
        for w in ["hello", "world"] {
            tx.send(w.to_string()).expect("Channel closed");
        }
    });
    thread::spawn(move || {
        for w in ["from", "rust"] {
            tx2.send(w.to_string()).expect("Channel closed");
        }
    });

    let words: Vec<String> = rx.iter().collect();
    println!("Number of words received: {}", words.len());
}

Learn this lesson with a mentor

Work through the Rust track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now