Async Rust Explained - Part 1
by Daniel Boros
Oct 3
4 min read
33 views
The general opinion about Rust is often divided. Those who use it, like us here on the blog, can hardly imagine a better language. Others, however, have mixed feelings about it. But if we set these subjective opinions aside, it is undeniable that Rust is fast, prioritizes memory safety, and ensures developers write safe code. For those experienced in Rust, the process is both challenging and rewarding, and it significantly enhances one's development skills.
So, let’s assume we have already started our Rust journey. We have learned the basics of the syntax, heard about references, pointers, a few smart pointers, and maybe even have a basic idea of lifetimes. Sooner or later, we’ll want to build something in the mainstream of modern development—for instance, a web server (excluding the frontend, of course). We decide to take the plunge and start developing a server in Rust.
In our previous posts, we discussed some of the tools you might want to use, but let’s quickly recap: Tokio, Axum, Actix, Async GraphQL, SurrealDB, Diesel-rs—there are plenty of excellent community-maintained packages to help along the way. Yet there is one hurdle to overcome: delving into the mysteries of Async Rust.
Because Rust is all about memory safety, every piece of code that isn’t explicitly marked as unsafe
must meet the compiler’s strict requirements—things like type correctness, proper reference handling, and lifetimes. We also need to ensure thread safety.
What is Thread Safety?
In Rust, we use two main traits for this: Send
and Sync
.
- A type is
Send
if it is safe to send it to another thread. - A type is
Sync
if it is safe to share between threads (T
isSync
if and only if&T
isSend
).
You can read more about these traits here: Send and Sync in Rust.
These traits are essential in Async Rust if we want to go beyond simply cloning everything and start using the language as it was intended to be used.
Async/Await Syntax
The Rust standard library does not support asynchronous programming directly, which is why we use libraries like Tokio, Futures, Async-Std, or Smol. But how can we achieve async/await syntax? The Rust compiler allows us to use this syntax with any function that returns an impl Future<Output = ()>
type. But what does that mean? We need a value that implements the Future
trait, and the Output
generic type could be any type (in our example, it is the unit type, but it could be anything).
Let’s look at an example where we implement a trait for a struct.
use futures::Future;
use std::task::{Context, Poll};
use std::pin::Pin;
struct MyFuture;
impl Future for MyFuture {
type Output = usize;
fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {
let is_ready = true;
if is_ready {
Poll::Ready(0)
} else {
Poll::Pending
}
}
}
fn my_future() -> impl Future<Output = usize> {
MyFuture
}
#[tokio::main]
async fn main() {
let result = my_future().await;
println!("Result: {}", result);
}
The Future
trait can be imported from the futures
crate, and we need to define a poll
method. We then create a function that returns MyFuture
, which implements the Future
trait. From this point, the compiler will allow us to use the async/await syntax for our function.
Another, simpler method is to return an async
or an async move
block.
fn my_future() -> impl futures::Future<Output = usize> {
async { 0 }
}
fn my_future_move() -> impl futures::Future<Output = usize> {
async move { 0 }
}
From the examples above, we see how we interpret async/await in Rust. These are basic introductory examples, but Async Rust is much deeper, and there's a lot more to explore. However, we need to start somewhere.
In the beginning of this post, we mentioned many other terms. We will cover these in future parts of this series. For now, let’s digest this.
Let’s Talk About Pin
Pin
is a particularly challenging part of Async Rust. We will dedicate an entire post to this topic in the future, but for now, let’s provide a brief overview so it doesn’t remain completely unexplained. Since Rust prioritizes memory safety above all, we need to ensure that references remain in place during an async method. This is where Pin
comes into play. Pin
tells the compiler not to move a reference in memory until the program is done running. Since Tokio and other async runtimes cleverly manage threads, when we start an async method, it’s possible that the runtime might move it to a different thread. In such cases, the references could shift, and the internal state could point to a memory address that no longer exists, resulting in the kind of undefined behavior well-known from C++.
For more detailed information on pinning, check out this link: Pinning in Rust
Conclusion
Although we are only scratching the surface of async Rust, it is clear that Rust is a complex and challenging language. However, for those who are dedicated and interested in high-performance programming, and who appreciate type safety and memory safety, Rust will definitely be appealing.