Concurrency

Threads, Send and Sync, and channels that move a value from one thread to another instead of sharing it.

Concurrency

Arc and Mutex are legal to share. A closure can move. A lifetime can be 'static. This lesson puts those together: start a thread, then pass messages so you do not need a shared mutex for every job. Async, which waits on I/O without one operating-system thread per wait, is the following lesson.

Threads, Send, and Sync

thread::spawn takes a move closure and returns a handle. join waits for that thread and gives back its result. The closure must be Send, and everything it owns must live for 'static, because the thread might outlive the function that started it. A borrow of a local will not compile. thread::scope joins every spawned thread before the scope ends, so those threads may borrow the parent’s stack.

Auto traitWhat it allowsExample
SendOwnership may move to another threadMost owned values. Rc is not Send.
SyncA shared reference may be used from many threadsArc is both when the inner value is. An Arc of a Mutex is what you clone per worker when they share a counter.
mainspawnsworker 1owns its dataworker 2owns its dataspawnspawn
Statusspawn moves the closure

Send means ownership may move to another thread. Sync means a shared reference may be used from many threads. Rc is neither. Arc is both when the inner value is. An Arc of a Mutex, from the previous lesson, is what you clone per worker when they must share a counter.

A data-parallel iterator library such as rayon is a common way to split CPU work. This lesson stays on the standard library so the spawn and the join stay visible.

01_threads_send_sync.rsRust
use std::thread;
use std::time::Duration;

fn main() {
    let handle = thread::spawn(|| {
        thread::sleep(Duration::from_millis(10));
        7
    });
    let answer = handle.join().unwrap();
    let data = vec![1, 2, 3];
    thread::scope(|scope| {
        scope.spawn(|| {
            println!("{}", data.len());
        });
    });
    println!("{answer}");
}

Channels

mpsc is many producers and one consumer. Sending moves the value. The sender no longer has it, which is ordinary ownership applied to a queue.

CallWhat the sender does
unbounded channelGrows if the consumer falls behind
sync_channel(n)Blocks the sender when n messages are waiting
try_recvReturns immediately if the queue is empty
recv_timeoutWaits only until the timeout
producersendsproducersendschannelowns messagesreceiverone
StatusSend moves the message

tx.clone() makes another producer. Each closure moves its sender. After send, that thread does not still own the String.

02_channels.rsRust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();
    let tx2 = tx.clone();
    thread::spawn(move || tx.send(String::from("one")).unwrap());
    thread::spawn(move || tx2.send(String::from("two")).unwrap());
    let first = rx.recv().unwrap();
    let second = rx.recv().unwrap();
    println!("{first} {second}");
}