Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Work in progress

Concurrency

Rust provides language and library features for writing concurrent programs. These features are designed to prevent data races — situations in which multiple threads access the same memory without proper synchronization, with at least one of the accesses modifying that memory.

This chapter describes the traits, types, and concepts that Rust uses to express and enforce safe concurrency.

Send and Sync

The Send and Sync traits are unsafe auto traits used by the Rust type system to track which types can be safely used across thread boundaries.

These traits are marker traits with no methods. Implementing them asserts that a type has the intrinsic properties required for safe concurrent use. The compiler automatically implements these traits for most types when possible, but they can also be implemented manually. Because other unsafe code may rely on these traits being correctly implemented, providing an incorrect manual implementation can cause undefined behavior.

Some types, such as Rc, UnsafeCell, Cell, and RefCell, intentionally do not implement Send or Sync. These types enable unsynchronized shared mutable state, which would be unsafe to transfer or share across threads.

Send

The Send trait indicates that ownership of values of a type can be safely transferred between threads.

  1. A type that implements Send can be moved to another thread and used there without causing data races or other undefined behavior.
  2. The Rust compiler automatically implements Send for types that satisfy its requirements (see the Send documentation for examples of types that automatically implement this trait).
  3. Manually implementing Send is unsafe. Such implementations must ensure that moving a value of that type to another thread cannot violate Rust’s aliasing or mutability guarantees.

Send is an [auto trait]: the compiler automatically implements it for types that meet its requirements. Most primitive types, such as integers and booleans, are Send. Types composed entirely of Send components (such as structs and enums whose fields are all Send) are also Send.

Types that manage non-thread-safe resources (such as raw pointers or unsynchronized interior mutability) may explicitly opt out of Send by providing a negative implementation (!Send).

#![allow(unused)]
fn main() {
struct SpecialThreadToken(u8);

impl !Send for SpecialThreadToken {}
}

Sync

The Sync trait indicates that references (&T) to a type can be safely shared between threads.

  1. If a type (T) is Sync, then (&T) is Send: immutable references to type (T) can be sent to other threads and accessed there concurrently.
  2. The Rust compiler automatically implements Sync for types that satisfy its requirements (see the Sync documentation for examples of types that automatically implement this trait).
  3. Manually implementing Sync is unsafe. Such implementations must ensure that concurrent shared access to values of that type cannot lead to data races or other undefined behavior.

Like Send, Sync is an [auto trait]: the compiler automatically implements it for types that meet its requirements. Most primitive types are Sync, and types composed entirely of Sync components are also Sync.

Types with interior mutability that is not synchronized for concurrent access may explicitly opt out of Sync by providing a negative implementation (!Sync).

#![allow(unused)]
fn main() {
struct SpecialThreadToken(u8);

impl !Sync for SpecialThreadToken {}
}

Atomics

Atomic types allow multiple threads to safely read and write shared values without using explicit locks by providing atomic operations such as atomic loads, stores, and read-modify-write with configurable memory ordering.

Atomic operations are guaranteed to be indivisible: no other thread can observe a value half-written or perform a conflicting update in the middle of an atomic operation. Correct use of atomic types can prevent data races, but misuse may still cause higher-level concurrency bugs such as deadlocks or livelocks.

The following table lists the atomic types and the corresponding primitive types they represent:

Atomic types are Sync, meaning references to them can be safely shared between threads. Using atomic operations correctly may require careful reasoning about memory ordering.

Asynchronous Computation

Rust provides asynchronous computation through the core::future::Future trait and core::task module. Asynchronous programming enables computations that may pause and resume without blocking the current thread.

A future represents a value that may not have finished computing yet. Any type implementing core::future::Future can be used as a future. Futures are lazy: calling an async function or an async closure returns a future but does not start computation until it is polled.

The result of a future is obtained in one of two ways:

  1. Using an await expression (future.await), which implicitly polls the future until it is ready.
  2. By explicitly invoking core::future::Future::poll.

Once a future has returned core::task::Poll::Ready, it must not be polled again. Doing so may panic, block forever, or cause other kinds of problems.

Async Closures

Closures may be marked with the async keyword, indicating that they produce a future when called. Calling an async closure does not perform its body immediately; instead, it returns a future representing the computation.

#![allow(unused)]
fn main() {
// An async function that accepts an async closure (something implementing AsyncFn(u64))
async fn takes_async_callback(f: impl AsyncFn(u64)) {
    f(0).await;
    f(1).await;
}

async fn example() {
    // Pass an async closure that prints its input
    takes_async_callback(async |i| {
        // This async closure just awaits on a ready-made future that returns its input
        core::future::ready(i).await;
        println!("done with {i}.");
    }).await;   // Await the entire `takes_async_callback` future to drive it to completion
}
}

2018 Edition differences

Async closures are available beginning with Rust 2018.

Async Closure Traits

Async closures implement the AsyncFn, AsyncFnMut, and AsyncFnOnce traits in a manner analogous to how regular closures implement Fn, FnMut, and FnOnce. Which traits are implemented depends on how variables are captured and whether the returned future needs to hold onto those captures.

An async closure is said to be lending to its future if:

  • It includes a mutable capture, or
  • It captures a value by move (by value), except when the value is accessed only via a dereference projection.

If an async closure is lending to its future:

  • It does not implement Fn or FnMut.
  • It always implements FnOnce.
Capture kindTraits implemented
Only immutable borrowsAsyncFn, AsyncFnMut, AsyncFnOnce
Contains a mutable borrowAsyncFnOnce only
Moves (captures by value)AsyncFnOnce only
Moves, but values are accessed via * (dereference projection)Same as immutable borrows (AsyncFn, AsyncFnMut, AsyncFnOnce)

Examples:

Mutable capture preventing FnMut:

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl FnMut() -> Fut) {}

fn f() {
    let mut x = 1i32;
    let c = async || {
        x = 2; // `x` captured mutably
    };
    takes_callback(c); // ERROR: async closure does not implement `FnMut`
}
}

By-value capture preventing Fn:

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}

fn f() {
    let x = &1i32;
    let c = async move || {
        let a = x + 2; // `x` captured by value
    };
    takes_callback(c); // ERROR: async closure does not implement `Fn`
}
}

Dereference projection allowing Fn:

#![allow(unused)]
fn main() {
fn takes_callback<Fut: Future>(c: impl Fn() -> Fut) {}

fn f() {
    let x = &1i32;
    let c = async move || {
        let a = *x + 2; // accessed via dereference
    };
    takes_callback(c); // OK: implements `Fn`
}
}

Combining async and unsafe

It is legal to declare a function that is both async and unsafe. Such a function is unsafe to call and, like any other async function, returns a future. The returned future is an ordinary future, and no unsafe context is required to await it.

The safety requirements of an async unsafe fn apply from the point of the call until the returned future has completed. This is because the body of an async function is suspended across yield points, so callers must ensure that all unsafe preconditions remain true for the entire duration of the returned future.

#![allow(unused)]
fn main() {
// Returns a future that, when awaited, dereferences `x`.
//
// Soundness condition: `x` must remain valid to dereference
// until the resulting future is complete.
async unsafe fn unsafe_example(x: *const i32) -> i32 {
    *x
}

async fn safe_example() {
    let p = 22;

    // An `unsafe` block is required to invoke the function:
    let future = unsafe { unsafe_example(&p) };

    // No `unsafe` block is required to await the future:
    let q = future.await;
}
}

This behavior follows from the desugaring of an async fn into a function that returns an impl Future. The unsafe qualifier applies to the call of that function, not to operations on the returned future.