Concurrency in Rust

Rust is a concurrent language. Safe concurrency and parallelism are its main strengths. The compiler ensures that your ownership and borrowing semantics are correct. The library enables you to postpone some of the checks from static analysis to the runtime. In all cases the coherence of your program is statically checked.

Memory management and data races

Ownership prevents objects from double-free bugs and (mostly) also prevents memory leaks. In other words, unless keep them alive by creating circular references using shared pointers like Arc or leak memory explicitly, all your objects will be dropped and freed exactly once.

Borrow checking prevents data races as well as use-after-free bugs. In other words, the borrow checker ensures that you either use aliasing (multiple &-references to a value) or mutation (single &mut-reference to a value). As a special case the Copy trait makes it possible to implement trivial objects that copy instead of moving just numbers do.

Simple threads with moved values

Threads and closures can be used together to from simple threaded functions with input and output values. Once the function is called, the thread is started and computation (or just waiting in our example) begins. The caller can wait and fetch the resulting value using .join().

use std::{thread, time::Duration, time::SystemTime};

fn delayed_value(sleep: u64, value: i32) -> thread::JoinHandle<i32> {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(sleep));
        value
    })
}

fn main() {
    println!("{:?}", SystemTime::now());
    let result = delayed_value(2, 42).join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{}", result)
}

It is easy to run multiple threads in parallel and then wait for the results of all of them at once.

fn main() {
    println!("{:?}", SystemTime::now());
    let thread1 = delayed_value(3, 42);
    let thread2 = delayed_value(2, 43);
    let result1 = thread1.join().unwrap();
    let result2 = thread2.join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{} {}", result1, result2)
}

What if you want to pass a common input value? In the following snippet you can only pass the value to two different functions because i32 is a Copy type and therefore its value gets copied to each function’s value argument.

fn main() {
    let value = 42;
    println!("{:?}", SystemTime::now());
    let thread1 = delayed_value(3, value);
    let thread2 = delayed_value(2, value);
    let result1 = thread1.join().unwrap();
    let result2 = thread2.join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{} {}", result1, result2)
}

You cannot do the same with non-Copy types like String. But you can still copy a string using .clone() which performs a deep copy of the data structure. Trivial types use memory-based Copy, more complex type provide a Clone implementation.

use std::{thread, time::Duration, time::SystemTime};

fn delayed_value(sleep: u64, value: String) -> thread::JoinHandle<String> {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(sleep));
        format!("Answer is {}.", value)
    })
}

fn main() {
    let value = "42".to_string();
    println!("{:?}", SystemTime::now());
    let thread1 = delayed_value(3, value.clone());
    let thread2 = delayed_value(2, value.clone());
    let result1 = thread1.join().unwrap();
    let result2 = thread2.join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{} {}", result1, result2)
}

Shared immutable data

What if you need to share a data structure rather than clone it? Maybe it’s large, maybe you’ll need to implement shared mutation later. If instead of cloning a String you clone an Arc<String>, the string itself is created only once and each .clone() just increments its reference count.

use std::{thread, time::Duration, time::SystemTime};
use std::sync::Arc;

fn delayed_value(sleep: u64, value: Arc<String>) -> thread::JoinHandle<String> {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(sleep));
        format!("Answer is {}.", value)
    })
}

fn main() {
    let value = Arc::new("42".to_string());
    println!("{:?}", SystemTime::now());
    let thread1 = delayed_value(3, value.clone());
    let thread2 = delayed_value(2, value.clone());
    let result1 = thread1.join().unwrap();
    let result2 = thread2.join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{} {}", result1, result2)
}

Why don’t we just use reference? Because it’s not all that easy. Rust doesn’t currently support scoped threads and thus thread::spawn() requres a 'static closure. That means we could only pass references with a 'static lifetime which may not always be what we want to do.

Shared mutable data

Technically, the borrow checker prevents you from sharing mutable data. But in the real world you’d like to distinguish sharable versus non-sharable rather than mutable versus immutable.

In other languages you often share data together with a mutex to resolve concurrent access. Therefore you want a Mutex<T> that is sharable (immutable in Rust) but that still provides access to a &mut T. This is called interior mutability. For the borrow checker the Mutex<T> looks immutable but a locked mutex enables mutable access to its inner value.

use std::{thread, time::Duration, time::SystemTime};
use std::sync::{Arc, Mutex};

fn delayed_value(sleep: u64, value: i32, results: Arc<Mutex<Vec<String>>>) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(sleep));
        results.lock().unwrap().push(format!("Answer is {}.", value));
    })
}

fn main() {
    let results = Arc::new(Mutex::new(Vec::new()));
    println!("{:?}", SystemTime::now());
    let thread1 = delayed_value(3, 42, results.clone());
    let thread2 = delayed_value(2, 43, results.clone());
    thread1.join().unwrap();
    thread2.join().unwrap();
    println!("{:?}", SystemTime::now());
    println!("{:?}", results.lock().unwrap())
}

You might wonder why we need to use such a complex type as Arc<Mutex<Vec<String>>>. Well, the core value is a Vec<String> so that you can .push() new results to it. It is wrapped by a Mutex<_> so that you can .lock().unwrap() to get exclusive access to it. This .lock() method only fails when another thread dies while holding the mutex.

Why don’t we pass use the Mutex<_> directly? We could use &Mutex<_> to share the mutex across threads but then the threads aren’t scope and would require a &'static Mutex<_> reference. We already solved the same problem using Arc<_> for immutable data. As Mutex<_> is considered immutable, let’s just do the same for the mutex and pass data around in Arc<Mutex<_>>.

Rust’s mutex wrapper feature

Most programming languages treat mutex as a cooperative tool to synchronize critical sections that are then used to synchronize access to data. Rust is different. It wraps the data by the mutex so that it is only accessible in the critical section. This allows the borrow checker to statically check the correctness of the code.

As long as you wrap your data in Mutex<_> and Arc<_>, it is passed around via .clone() and data access guarded using .lock(). Only in a critical section the data is available.

// This block is your critical section
{
    let data = mutex.lock().unwrap();
    data.do_whatever_you_want();
    data.do_whatever_you_want();
    data.do_whatever_you_want();
}

Channels

Sharing data using a mutex isn’t the final answer to all your questions. Locking may have additional performance impact and you need to use additional tools like CondVar to introduce events.

If you just need to feed a thread with events or messages, a queue (or channel) based approach may serve you better.

use std::{thread, time::Duration, time::SystemTime};
use std::sync::mpsc::{SyncSender, sync_channel};

fn delayed_value(sleep: u64, value: i32, sender: SyncSender<String>) -> thread::JoinHandle<()> {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(sleep));
        sender.send(format!("Answer is {}.", value)).unwrap();
    })
}

fn main() {
    let (sender, receiver) = sync_channel(32);

    let thread1 = delayed_value(3, 42, sender.clone());
    let thread2 = delayed_value(2, 43, sender.clone());

    // Here in the main thread...
    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().unwrap());
    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().unwrap());
    println!("{:?}", SystemTime::now());

    thread1.join().unwrap();
    thread2.join().unwrap();
}

Cooperative tasks and functions

Threads are managed by the operating system scheduler and thread management has overhead of its own. In a high-performance setting you usually do not want to create new threads for the delayed tasks.

Asynchronous functions in Rust allow you to express your code in a way that works with threads but at the same time doesn’t force a particular way. You can use the Tokio library to glue your asynchronous to a single-threaded cooperative application or an optimized thread pool scheduler.

A naive rewrite of the above threaded example follows.

use std::{time::Duration, time::SystemTime};
use tokio::task;
use tokio::sync::mpsc::{Sender, channel};

fn delayed_value(sleep: u64, value: i32, sender: Sender<String>) -> task::JoinHandle<()> {
    task::spawn(async move {
        tokio::time::sleep(Duration::from_secs(sleep)).await;
        sender.send(format!("Answer is {}.", value)).await.unwrap();
    })
}

#[tokio::main]
async fn main() {
    let (sender, mut receiver) = channel(32);

    let task1 = delayed_value(3, 42, sender.clone());
    let task2 = delayed_value(2, 43, sender.clone());

    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().await.unwrap());
    println!("{:?}", SystemTime::now());
    println!("{}", receiver.recv().await.unwrap());
    println!("{:?}", SystemTime::now());

    task1.await.unwrap();
    task2.await.unwrap();
}

We can create much simple programs with asynchronous functions.

use std::{time::Duration, time::SystemTime};
use tokio::join;

async fn delayed_value(sleep: u64, value: i32) -> String {
    tokio::time::sleep(Duration::from_secs(sleep)).await;
    format!("Answer is {}.", value)
}

#[tokio::main]
async fn main() {
    println!("{:?}", SystemTime::now());
    let (result1, result2) = join!(
        delayed_value(3, 42),
        delayed_value(2, 43),
    );
    println!("{} {}", result1, result2);
    println!("{:?}", SystemTime::now());
}

The main reasons to use asynchronous functions are flexibility and readability. You write the code once, you decide the threading configuration later.