Rust’s type system makes data races compile-time errors in safe code: a value can only be sent to or shared with another thread if its type says that is safe. Rust offers both operating-system threads and asynchronous tasks. A data race is undefined behaviour, so these rules are part of memory safety. Deadlocks are not prevented.
Send and Sync
Send and Sync are
auto traits and
unsafe traits (Reference: Special
types and traits).
| Trait | Means |
|---|---|
Send | A value of the type can be moved to another thread |
Sync | A value of the type can be shared between threads, so T: Sync exactly when &T: Send |
| Type | Send | Sync |
|---|---|---|
| Primitive types | Yes | Yes |
Rc<T> | No | No |
Arc<T> | If T: Send + Sync | If T: Send + Sync |
Cell<T>, RefCell<T> | If T: Send | No |
Mutex<T> | If T: Send | If T: Send |
*const T, *mut T | No | No |
&T | If T: Sync | If T: Sync |
A type that holds raw pointers can opt in with unsafe impl Send for T {},
promising that doing so is safe. Types used in a non-mut
static must be Sync.
Threads
use std::thread;
let handle = thread::spawn(|| {
println!("hello from a thread");
42
});
let result = handle.join().unwrap(); // waits, and returns the closure's value
let data = vec![1, 2, 3];
thread::scope(|s| { // threads may borrow local data
s.spawn(|| println!("{:?}", data));
s.spawn(|| println!("{}", data.len()));
}); // all scoped threads are joined here
thread::spawn needs a closure and return value that are
Send and 'static, usually written with move.
Scoped threads may borrow from the enclosing function. A panic in a spawned thread
makes join return Err.
Thread-local storage is declared with the thread_local! macro.
Sharing and synchronisation
| Type | Purpose |
|---|---|
std::sync::Arc<T> | Reference-counted shared ownership across threads |
std::sync::Mutex<T> | Mutual exclusion; lock returns a guard that releases on drop |
std::sync::RwLock<T> | Many readers or one writer |
std::sync::Condvar, Barrier | Waiting for conditions and rendezvous |
std::sync::mpsc | Multi-producer, single-consumer channels |
std::sync::Once, OnceLock, LazyLock | One-time initialisation |
std::sync::atomic | Atomic integers, booleans and pointers |
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..4 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
*c.lock().unwrap() += 1;
}));
}
for h in handles { h.join().unwrap(); }
A Mutex is poisoned when a thread panics while holding it, and
later lock calls return Err. Atomic operations take a
memory ordering: Relaxed, Acquire, Release,
AcqRel or SeqCst. Rust atomics currently follow the same
rules as C++20 atomics.
Async functions and blocks
async is available from
edition 2018. Calling an
async fn does no work at once: it captures its arguments into a value
implementing Future, and the body runs when that future is polled. The
standard library has no executor, so running futures needs a runtime crate such as
Tokio or smol.
async fn fetch() -> u32 { 1 }
// Roughly:
// fn fetch() -> impl Future<Output = u32> { async move { 1 } }
async fn run() -> u32 {
let x = fetch().await;
let y = async { x + 1 }.await; // an async block
y
}
- An
async fn’s return type captures all lifetime parameters, and its body captures all parameters, even unused ones. - An
asyncblock produces a future;async movemoves its captures in. - Async functions may be declared in traits, but such traits are not dyn compatible.
- Async closures implement the
AsyncFn,AsyncFnMutandAsyncFnOncetraits.
.await
AwaitExpression: Expression . await
.await is legal only in an async function, closure or block. Its operand
must implement IntoFuture. Evaluating operand.await calls
IntoFuture::into_future, pins the future, and polls it. If
poll returns Poll::Pending, the enclosing future also
returns Poll::Pending and suspends; when polled again it resumes at
the poll. If poll returns Poll::Ready(v), v is
the value of the expression (Reference: Await expressions).
The Future trait
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}
A future is a state machine that an executor polls. When it is not ready, it returns
Poll::Pending and arranges to wake the task through the
Waker in cx. A future must not be polled after it has returned
Ready; what happens then is up to the implementation. Future
and IntoFuture are in the
prelude in edition 2024.
Pin and Unpin
An async state machine may hold references into itself across an
.await, so it must not move once polling begins.
Pin<P> wraps a pointer and promises that the target will not be moved
unless its type is Unpin.
Unpinis an auto trait. Most types areUnpin, so pinning has no effect on them. Futures fromasynccode usually are not.Box::pin(value)pins on the heap. Thestd::pin::pin!macro pins on the stack.Pin::newworks only forUnpintargets.Pin::new_uncheckedisunsafe: the caller promises to keep the value in place.