Rust allows running multiple tasks at once with safety guarantees at compile time. You cannot create race conditions without the compiler preventing you.
Threads
They are created with std::thread::spawn. Each thread runs a closure in parallel.
use std::thread;
thread::spawn(|| {
println!("Hello from another thread");
});Waiting for a thread to finish
thread::spawn returns a JoinHandle. Call .join() to block the main thread until the other one finishes:
use std::thread;
let thread = thread::spawn(|| {
println!("Hello from another thread");
});
thread.join().unwrap(); // Blocks until the thread finishesCapturing variables with move
For a thread to access data from the main thread, you need to transfer ownership with move. See Ownership and move semantics.
use std::thread;
let message = String::from("Hello");
let thread = thread::spawn(move || {
println!("{}", message); // Takes ownership of 'message'
});
thread.join().unwrap();
println!("{}", message); // ERROR: message was moved into the threadNote
Without
move, Rust would not allow usingmessagein the thread because it could not guarantee that the data stays alive while the thread uses it. Withmove, the thread takes ownership and is responsible for freeing it.
Communication between threads
Channels (mpsc) allow sending messages between threads. mpsc stands for multiple producer, single consumer.
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel(); // tx = transmitter; rx = receiver
thread::spawn(move || {
tx.send(String::from("Hello")).unwrap();
});
println!("{}", rx.recv().unwrap());Multiple messages
Multiple messages can be sent with a loop:
use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
// Sender (in another thread)
thread::spawn(move || {
for i in 1..=5 {
tx.send(i).unwrap();
}
});
// Receiver (main thread)
for received in rx {
println!("{}", received);
}Note
The
for received in rxloop blocks until each message is received. When theSenderis destroyed (the sender thread finishes), the loop ends automatically.
Async
async functions allow executing tasks asynchronously without blocking the thread. They do not create a new thread; the runtime manages multiple tasks in a pool of threads.
Futures
An async function returns a Future:
async fn read() -> String {
// ...
}The compiler transforms it approximately into:
fn read() -> impl Future<Output = String> {
// ...
}Important
Calling an
asyncfunction does not execute its code; it only creates theFuture. The runtime starts executing it when.awaitis called on it or when it is handed over to the runtime (for example, withtokio::spawnortokio::join!).
How to execute a Future
hello(); // Does not print anything (only creates the Future)
hello().await; // Executes the function and waits for it to finish
let read = read().await;Warning
.awaitcan only be used inside anasynccontext. You cannot.awaitin a normalfnfunction.
Important
When
.awaitis called, the runtime starts executing theFutureuntil it produces a result. If it needs to wait, it switches to another task.
Runtime
To execute an async function, an async runtime is needed. The most popular ones are Tokio, async-std and smol.
#[tokio::main]
async fn main() {
hello().await;
}Note
#[tokio::main]is an attribute macro that transforms the normalmaininto anasync mainwith the Tokio runtime.
Running a task in the background
tokio::spawn launches an async task in the background while other tasks run, and returns a JoinHandle to wait for its result later.
JoinHandle.await waits for the task if it has not finished and returns a Result<T, JoinError>. See Option and Result.
JoinHandle.await.unwrap() extracts the data or panics if there is an error (Not recommended).
async fn download() -> String {
println!("Start of the task");
String::from("Data")
}
#[tokio::main]
async fn main() {
let task = tokio::spawn(download());
println!("Meanwhile I can do other things");
match task.await {
Ok(data) => println!("{data}"),
Err(e) => println!("The task failed: {e}"),
}
}Note
tokio::spawncreates a new async task, not a new thread. The runtime decides which thread to run it on.
Running multiple tasks
tokio::join! runs the Futures passed as arguments concurrently and waits for all of them to finish.
async fn task1() -> i32 {}
async fn task2() {}
#[tokio::main]
async fn main() {
let t1 = task1();
let t2 = task2();
let (result1, _) = tokio::join!(t1, t2);
// It stops until they all finish
}Note
tokio::join!returns a tuple with the return values of all theFutures.
Important
tokio::join!does not create a new task; it alternates polling between theFutures until they finish.
Error handling
If one Future fails, the other keeps running.
To cancel all of them if one fails, use tokio::try_join!.
tokio::try_join! only works with Futures that return Result and they all must have the same error type.
Its return value is a Result with a tuple of all the Ok values and the common Err.
async fn task1() -> Result<String, Error> {}
async fn task2() -> Result<i32, Error> {}
match tokio::try_join!(task1(), task2()) { // Result<(String, i32), Error>
Ok((r1, r2)) => {
println!("{r1}");
println!("{r2}");
}
Err(e) => {
println!("One task failed: {e}");
}
}Task switching
The runtime executes a task until it yields control. This normally happens when .await is called on a Future that has not finished yet.
At that point, the task is suspended and the runtime can run another task while waiting.
async fn task1() {
println!("Task 1: start");
sleep(Duration::from_secs(1)).await;
println!("Task 1: continues");
}
async fn task2() {
println!("Task 2: start");
sleep(Duration::from_secs(1)).await;
println!("Task 2: continues");
}
#[tokio::main]
async fn main() {
tokio::join!(task1(), task2());
}A possible output would be:
Task 1: start
Task 2: start
Task 1: continues
Task 2: continuesNote
awaitdoes not block the thread. It simply suspends the current task so the runtime can run others while waiting.
How it works:
- The runtime starts executing
task1. task1reachessleep(...).await.- Since the timer has not finished yet,
task1is suspended. - Same for
task2. - When one of the timers finishes, the runtime resumes the corresponding task.
- Finally both tasks finish.
Important
A task only yields control when it does
.awaiton aFuturethat is not ready yet. If theFuturehas already finished, execution continues immediately.
Note
The exact order in which tasks run is not guaranteed and can vary on each execution.
Summary
| Feature | future.await | tokio::join! | tokio::spawn |
|---|---|---|---|
Executes a Future | ✓ | ✓ | ✓ |
Number of Future | 1 | Several | 1 per spawn |
| Creates a new task | X | X | ✓ |
main can continue running immediately | X | X | ✓ |
| Runs concurrently | X | ✓ | ✓ |
| Can run on different threads | X | X (same task) | ✓ (if the runtime is multithreaded) |
| Returns | T | (T1, T2, ...) | JoinHandle<T> |
.await needed to get the result | ✓ | join! itself waits | ✓ on the JoinHandle |
| Typical use | Wait for one operation | Wait for several related operations | Launch independent work in the background |