In Rust, futures are a core concept of asynchronous programming. A Future represents a value that might not yet be available but will be produced at some point in the future. Think of it as a promise that something will complete, like fetching data from the internet or reading a file, without blocking the rest of the program.
What Are Futures in Rust?
A Future is an object that represents a computation that hasn’t finished yet but will produce a value eventually. It’s like ordering food at a restaurant—you place your order (start a future), and when the food is ready, the server delivers it (the future resolves).
Futures are the foundation of Rust’s asynchronous programming model. They make it possible to write non-blocking code that runs efficiently without holding up other tasks.
Key Characteristics of Futures
- Lazy Execution: Futures in Rust don’t do anything until they are awaited or explicitly polled.
- Non-Blocking: Futures allow the program to perform other tasks while waiting for the result.
- Poll-Based: Rust’s executor repeatedly checks (polls) the future to see if it’s ready.
How Futures Work in Rust
A Future implements the Future trait from the std::future module:
pub trait Future {
type Output; // The type of value the future will produce
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
You don’t need to write futures from scratch because Rust provides tools to create and manage them easily.
Creating a Future
You can create a simple future using the async keyword.
Example: A Simple Future
async fn my_future() -> u32 {
42
}
#[tokio::main]
async fn main() {
let result = my_future().await;
println!("The future resolved with: {}", result);
}
Explanation:
- async fn my_future: Creates a future that resolves to 42.
- .await: Waits for the future to complete and retrieves the result.
Chaining Futures with .then()
Futures can be chained together to perform sequential tasks.
Example: Chaining Futures
use futures::future;
#[tokio::main]
async fn main() {
let future = future::ready(10);
let result = future.then(|value| async move {
value * 2
}).await;
println!("Result: {}", result);
}
Output:
Result: 20
Combining Multiple Futures
You can combine multiple futures to run them concurrently using tools like join and select.
Example: Running Futures Concurrently
use tokio::time::{sleep, Duration};
async fn task_one() {
sleep(Duration::from_secs(1)).await;
println!("Task one completed");
}
async fn task_two() {
sleep(Duration::from_secs(2)).await;
println!("Task two completed");
}
#[tokio::main]
async fn main() {
tokio::join!(task_one(), task_two());
println!("Both tasks completed");
}
Output:
Task one completed
Task two completed
Both tasks completed
Explanation:
- tokio::join!: Runs both futures concurrently, waiting for both to finish.
Custom Futures
You can also define custom futures by implementing the Future trait.
Example: Custom Future
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
struct MyFuture {
state: u32,
}
impl Future for MyFuture {
type Output = u32;
fn poll(mut self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.state == 0 {
self.state += 1;
Poll::Pending // Not ready yet
} else {
Poll::Ready(42) // Ready with the result
}
}
}
#[tokio::main]
async fn main() {
let my_future = MyFuture { state: 0 };
let result = my_future.await;
println!("Custom future resolved with: {}", result);
}
Futures and Executors
Futures are lazy and require an executor to run them. Executors like Tokio or async-std repeatedly poll futures to check if they’re ready.
- Tokio: The most popular executor for async programming in Rust.
- async-std: Another runtime similar to Node.js’s async model.
Add tokio to your Cargo.toml to use it as the executor:
[dependencies]
tokio = { version = "1", features = ["full"] }
Error Handling in Futures
You can handle errors in futures using the Result type and .await.
Example: Future with Error Handling
async fn fetch_data() -> Result<String, &'static str> {
Ok("Data fetched successfully".to_string())
}
#[tokio::main]
async fn main() {
match fetch_data().await {
Ok(data) => println!("{}", data),
Err(e) => println!("Error: {}", e),
}
}