Rust / the practical course

03 / Concurrency

Channels, locks, and shutdown

Pass work through channels, protect shared data, and shut down without deadlocks.

55 min + practiceRust 1.98.1 · Edition 2024

By Robert DeVore · Download Markdown

Give the state a clear owner

A producer creates work; a consumer receives it and keeps the running total. A channel transfers each message to the consumer. This avoids multiple workers mutating the same summary and makes the shutdown condition visible.

Run the example

cargo run --locked --example 17_sync
use std::{sync::mpsc::sync_channel, thread};
fn main() {
    let (sender, receiver) = sync_channel(2);
    let worker = thread::spawn(move || {
        for value in [1, 2, 3] {
            sender.send(value).unwrap();
        }
    });
    let total: i32 = receiver.iter().sum();
    worker.join().unwrap();
    assert_eq!(total, 6);
}

View the tested source

The synchronous channel has capacity two. Sending can block when the queue is full. The consumer keeps receiving until every sender is dropped. Here the producer's sender is dropped on return, so iteration ends and joining is safe.

If the main thread retained an unused sender clone, the receiver could wait forever. If the main thread joined a blocked producer before draining the full channel, the program could deadlock. Both bugs can occur in fully safe Rust. Write down who closes the queue and who drains it before choosing the order of waits.

When a mutex is appropriate

A short update to a genuinely shared data structure can fit Arc<Mutex<T>>. The lock stays held for as long as its guard lives. Copy or move out the small result you need and release the guard before unrelated slow work. Avoid calling arbitrary callbacks while holding a lock unless the contract explicitly covers reentrancy and lock ordering.

Standard mutex poisoning indicates that a panic may have interrupted a protected operation. It is advisory and not a soundness mechanism. Recovery must examine application invariants; blindly calling into_inner is not evidence the state is usable. Likewise, an unpoisoned mutex is not proof that the algorithm is correct.

Acquire multiple locks in a consistent order where possible. A read-write lock can help some read-heavy patterns, but it is not automatically faster and can introduce different contention behavior. Draw who owns the data and measure contention before choosing a more complex lock.

What each layer controls

The language and standard library describe what data can be safely accessed and what synchronization establishes. The OS chooses which runnable threads execute. Hardware may reorder operations within constraints imposed by the language memory model. Application architecture decides whether messages can be retried or discarded. A program that works under one schedule may still fail under another.

Exercise

Draw the channel's close-and-drain sequence. Add a producer error path that returns early and confirm the consumer still terminates. Then describe how you would report an error without treating a partial aggregate as complete.

Solution and acceptance check

An early return drops that sender; if no senders remain, receiving eventually ends after queued messages drain. Joining the producer reveals whether it completed successfully. Return a status with the partial total, or reject the result. A disconnected channel alone does not prove the producer processed every intended item.

Sources: sync_channel, Mutex, and message-passing concurrency.

Find a lesson