The original title: Understanding Futures Rust – Part 1 In the original link: www.viget.com/articles/un… Praying for Rust
background
Futures in Rust are similar to promises in Javascript in that they are powerful abstractions from the concurrency primitives in Rust. This is also the cornerstone of async/await, which allows users to write asynchronous code as if they were writing synchronous code.
Async/await is not ready in the early days of Rust, but that doesn’t mean you shouldn’t start using futures in your Rust project. Tokio Crate is stable, easy to use, and fast. See this document for a primer on using the Future.
Futures is already in the standard library, but in this series of blogs, I’m going to write a simplified version that shows how it works, how to use it, and avoid some common pitfalls.
The main branch of Tokio is using STD :: Future, but all documentation references futures from version 0.1. However, these concepts are applicable.
Although futures is now in STD, many common features are missing. These features are currently maintained in Future-Preview, and I’ll reference the functions and traits defined in them. Things move quickly, and many of the items from that Crate end up in the standard library.
Preliminary knowledge
-
Learn something about Rust or be willing to learn about Rust along the way (or better yet, read the Rust Book).
-
A modern browser like Chrome, FireFox, Safari, or Edge (we’ll use Rust Playground)
-
That’s it!
The target
The goal of this article is to be able to understand the code below and implement the types and functions needed to compile it. This code is a valid syntax for library futures and shows how chained futures works.
// This code does not compile yet
fn main() {
let future1 = future::ok::<u32.u32> (1)
.map(|x| x + 3)
.map_err(|e| println!("Error: {:? }", e))
.and_then(|x| Ok(x - 3))
.then(|res| {
match res {
Ok(val) => Ok(val + 3),
err => err,
}
});
let joined_future = future::join(future1, future::err::<u32.u32> (2));
let val = block_on(joined_future);
assert_eq!(val, (Ok(4), Err(2)));
}
Copy the code
What exactly is Future?
Specifically, it is the value represented by a series of asynchronous calculations. Futures Crate’s documentation states that it “represents an object, This object is a concept for an object which is a proxy for another value that may not be ready yet.
Futures in Rust allows you to define a task that can be run asynchronously, such as a network call or computation. You can link functions on that result, transform it, handle errors, merge with other futures, and perform many other calculations. These functions are executed only when the future is passed to an executor, such as Tokio’s Run function. In fact, if you don’t use the Future before you leave the scope, nothing will happen. Therefore, Futures Crate states that futures are must_use, and the compiler will issue a warning if you allow them to leave scope without being used.
If you’re familiar with JavaScript Promises, some things might sound strange. In JavaScript, Promises are implemented in an event loop, and there is no other option to run them. Executor functions run immediately. At its core, however, promises still simply define a set of instructions to perform in the future. In Rust, executors can choose to run any of a number of asynchronous policies.
Building our Future
At a higher level, we need snippets of code to make futures work; A runner, future trait, and poll type.
First, a Runner
If we don’t have a way to implement our future, it won’t do anything. Because we are implementing our own futures, we also need to implement our own runner. In this exercise, we won’t actually do anything asynchronous, but we will make an approximate asynchronous call. Futures are based on pull rather than push. This allows futures to be a zero abstraction, but it also means that they are polled once and are responsible for alerting executor when they are ready to poll again. The specifics of how it works are not important to understand how futures are created and linked together, so our Executor is only a very rough approximation. It can only run one Future, and it can’t do any meaningful asynchrony. The Tokio documentation has a lot of information about the Futures runtime model.
Here is a seemingly simple implementation:
usestd::cell::RefCell; thread_local! (static NOTIFY: RefCell<bool> = RefCell::new(true));
struct Context<'a> {
waker: &'a Waker,
}
impl<'a> Context<'a> {
fn from_waker(waker: &'a Waker) -> Self {
Context { waker }
}
fn waker(&self) - > &'a Waker {
&self.waker
}
}
struct Waker;
impl Waker {
fn wake(&self) {
NOTIFY.with(|f| *f.borrow_mut() = true)}}fn run<F>(mut f: F) -> F::Output
where
F: Future,
{
NOTIFY.with(|n| loop {
if *n.borrow() {
*n.borrow_mut() = false;
let ctx = Context::from_waker(&Waker);
if let Poll::Ready(val) = f.poll(&ctx) {
returnval; }}})}Copy the code
Run is a generic function, where F is a future, and it returns a value of type Output defined in the Future trait, which we’ll cover later.
The logic of the function body approximates what a real runner might do, which loops until it is reminded that the future is ready to be polled again. It returns from the function when the future is ready. The Context and Waker types are simulations of the same type defined in the Future :: Task module, as you can see here. Compilation requires them to be there, but that is beyond the scope of this article. You’re free to explore how they work.
Poll is a simple generic enumeration, which we can define as follows:
enum Poll<T> {
Ready(T),
Pending
}
Copy the code
Our Trait
Traits are a way to define shared behavior in Rust. It allows us to specify the types and functions that the implementation type must define. It can also implement default behavior, which we’ll see when we talk about combinator.
Our trait implementation looks like this (which is consistent with the real Futures implementation) :
trait Future {
type Output;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output>;
}
Copy the code
For now, the trait is simple, declaring the required type — Output — and the signature of the only required method — poll, which holds a reference to a Context object. This object holds a reference to waker, which is used to alert the runtime that the future is ready to be polled again.
Our implementation
#[derive(Default)]
struct MyFuture {
count: u32,}impl Future for MyFuture {
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
match self.count {
3 => Poll::Ready(3), _ = > {self.count += 1;
ctx.waker().wake();
Poll::Pending
}
}
}
}
Copy the code
Let’s look at the above code line by line:
-
#[derive(Default)] Automatically creates a :: Default () function for this type. The numeric type (count here) defaults to 0.
-
Struct MyFuture {count: u32} defines a simple structure with a counter (count). This allows us to simulate asynchronous behavior.
-
Impl Future for MyFuture is our implementation of this trait.
-
We set Output to type I32 so we can return the internal count.
-
In our poll implementation, we decide what to do based on the internal count field,
-
If it matches 33=> we return a Poll::Ready response with a value of 3.
-
In other cases, we increment the counter and return Poll::Pending
With a simple main function, we can run our future!
fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(my_future));
}
Copy the code
Run it yourself!
The last step
That’s how it works, but it doesn’t really show you how powerful Futures is. So, let’s create a super-convenient Future that can increment by 1 by linking to any type that can increment by 1, for example, MyFuture.
struct AddOneFuture<T>(T);
impl<T> Future for AddOneFuture<T>
where
T: Future,
T::Output: std::ops::Add<i32, Output = i32{>,type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
match self.0.poll(ctx) {
Poll::Ready(count) => Poll::Ready(count + 1),
Poll::Pending => Poll::Pending,
}
}
}
Copy the code
This code looks complicated but is actually quite simple. I’ll go over it line by line again:
-
struct AddOneFuture
(T); This is an example of the generic NewType schema. It allows us to wrap other constructs and add our own behavior.
-
Impl
Future for AddOneFuture
is a generic trait implementation.
-
T: Future guarantees that anything in the AddOneFuture Wrap implements the Future.
-
T::Item: STD ::ops::Add
,>
ensures that the value represented by Poll::Ready(value) has a corresponding + operation.
The rest is easy to follow. It polls the internal future using self_0. Poll, throughout the context, and depending on the result either returns poll ::Pending or returns the count of the internal Future plus 1 — poll ::Ready(count + 1)
We can just update the main function to use our new future.
fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(AddOneFuture(my_future)));
}
Copy the code
Run it yourself!
Now we can see how we can use futures to link asynchronous behaviors together. It takes just a few simple steps to set up the chain functions that empower futures.
The profile
-
Future is a powerful way to leverage Rust’s zero-cost abstraction to achieve readable, fast asynchronous code.
-
Futures behaves much like promises in JavaScript and other languages.
-
We’ve learned a lot about building generic types and part of linking behaviors together.
The following
In Part 2, we’ll talk about combinators. Combinators, on the non-technical side, allow you to use functions (such as callback functions) to build a new type. If you’ve used JavaScript Promises, this will be familiar.