Inside of Rust async/.await

Keywords: Blockchain Programming REST network

In this tutorial, we will analyze the internal running mechanism of async/.await in detail. We will use the async STD library instead of tokio, because this is the first t rust library that supports async/.await syntax. The tutorial of async/.await principle analysis is divided into two parts, which is the first part.

Blockchain development tutorial link: Ethereum | Bitcoin | EOS | Tendermint | Hyperledger Fabric | Omni/USDT | Ripple

0. Prepare for the rule practice environment

First let's create a Cargo project:

~$ cargo new --bin sleepus-interruptus

If you want to be consistent with the compiler used in the tutorial, you can add a rust toolchain file with the content of 1.39.0.

Before continuing, run cargo run to make sure the environment is OK.

1. An alternating display of the rule program

We need to write a simple program, which can display 10 sleep us messages with an interval of 0.5 seconds each time, and 5 Interruptus messages with an interval of 1 second each time. The following is a fairly simple implementation code of rust:

use std::thread::{sleep};
use std::time::Duration;

fn sleepus() {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
}

fn interruptus() {
    for i in 1..=5 {
        println!("Interruptus {}", i);
        sleep(Duration::from_millis(1000));
    }
}

fn main() {
    sleepus();
    interruptus();
}

However, the above code will perform two operations synchronously. It will display all the Sleepus messages first, and then the Interruptus message. What we expect is that these two kinds of messages are interleaved, that is to say, Interruptus messages can interrupt the display of Sleepus messages.

There are two ways to achieve the goal of interleaved display. The obvious one is to create a separate thread for each function and wait for the thread to finish executing.

use std::thread::{sleep, spawn};

fn main() {
    let sleepus = spawn(sleepus);
    let interruptus = spawn(interruptus);

    sleepus.join().unwrap();
    interruptus.join().unwrap();
}

It should be noted that:

  • We use spawn(sleepus) instead of spawn(sleepus()) to create threads. The latter will immediately execute sleepus() and then pass its execution result to spawn, which is not what we expect - I use join() in the main function to wait for the end of the sub thread, and unwrap() to handle the faults that can occur because I am lazy.

Another implementation is to create a worker thread and call one of the functions in the main thread:

fn main() {
    let sleepus = spawn(sleepus);
    interruptus();

    sleepus.join().unwrap();
}

This method is more efficient, because only one additional thread needs to be created, and there is no side effect, so I recommend using this method.

But neither is an asynchronous solution! We use two threads managed by the operating system to perform two synchronization tasks concurrently! Next let's try to make two tasks work together in a single thread!

2. The realization of alternating display program by using rule asynchronous async/.await

We'll start at a higher level of abstraction, and then step into the details of asynchronous programming in rust. Now let's rewrite the previous application in the async style.

First of all Cargo.toml Add the following dependencies to:

async-std = { version = "1.2.0", features = ["attributes"] }

Now we can rewrite the application as:

use async_std::task::{sleep, spawn};
use std::time::Duration;

async fn sleepus() {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500)).await;
    }
}

async fn interruptus() {
    for i in 1..=5 {
        println!("Interruptus {}", i);
        sleep(Duration::from_millis(1000)).await;
    }
}

#[async_std::main]
async fn main() {
    let sleepus = spawn(sleepus());
    interruptus().await;

    sleepus.await;
}

The main modifications are as follows:

  • Instead of using sleep and spawn functions in std::thread, we use async_std::task. -Add async before sleepus and interruptus functions
  • After calling sleep, we added. await. Note that instead of the. await() call, it's a new syntax
  • Use ා [async] on the main function_ STD:: Main] property
  • async keyword before main function
  • Now we use spawn(sleepus()) instead of spawn(sleepus()), which means we call sleepus directly and pass the result to spawn
  • Call to interruptus() added. await
  • Instead of using join() for sleepus, use the. await syntax instead

There seem to be a lot of changes, but in fact, our code structure is basically the same as the previous version. Now the program runs in line with our expectation: using a single thread for non blocking calls.

Next, let's analyze what these changes mean.

3. The function of async keyword

Adding async before function definition mainly does the following three things:

  • This will allow you to use the. await syntax within the function body. We'll explore this in more depth next
  • It modifies the return type of the function. Async FN foo() - > bar actually returns impl STD:: Future:: future < output = bar >
  • It automatically encapsulates the resulting value into a new Future object. We will show this in detail below

Now let's expand on point 2. There is a trait named Future in the standard library of rule, and Future has an associated type Output. I promise that when I finish the task, I will give you a value of type Output. For example, you can imagine that an asynchronous HTTP client might be implemented as follows:

impl HttpRequest {
    fn perform(self) -> impl Future<Output=HttpResponse> { ... }
}

When sending HTTP requests, we need some non blocking I/O. we don't want to block the calling thread, but we need to get the final response result.

The result type of async fn sleepus() is implied as (). So our Future Output should also be (). This means that we need to modify the function to:

fn sleepus() -> impl std::future::Future<Output=()>

However, if you only modify here, the following errors will occur during compilation:

error[E0728]: `await` is only allowed inside `async` functions and blocks
 --> src/main.rs:7:9
  |
4 | fn sleepus() -> impl std::future::Future<Output=()> {
  |    ------- this is not `async`
...
7 |         sleep(Duration::from_millis(500)).await;
  |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ only allowed inside `async` functions and blocks

error[E0277]: the trait bound `(): std::future::Future` is not satisfied
 --> src/main.rs:4:17
  |
4 | fn sleepus() -> impl std::future::Future<Output=()> {
  |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
  |
  = note: the return type of a function must have a statically known size

The first error message is straightforward: you can only use the. await syntax in async functions or code blocks. We haven't touched the asynchronous code block yet, but it looks like this:

async {
    // async noises intensify
}

The second error message is the result of the first: the async keyword requires that the function return type be impl Future. Without this keyword, our loop result type is (), which obviously does not meet the requirements.

Wrap the whole function body with an asynchronous code block to solve the problem:

fn sleepus() -> impl std::future::Future<Output=()> {
    async {
        for i in 1..=10 {
            println!("Sleepus {}", i);
            sleep(Duration::from_millis(500)).await;
        }
    }
}

4. The role of. await syntax

Maybe we don't need all of these async/.await. What happens if we remove. await from sleepus? Surprisingly, the compilation passed, although a warning was given:

warning: unused implementer of `std::future::Future` that must be used
 --> src/main.rs:8:13
  |
8 |             sleep(Duration::from_millis(500));
  |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: `#[warn(unused_must_use)]` on by default
  = note: futures do nothing unless you `.await` or poll them

We are generating a Future value but are not using it. If you look at the output of the program, you can understand what the compiler's warnings mean:

Interruptus 1
Sleepus 1
Sleepus 2
Sleepus 3
Sleepus 4
Sleepus 5
Sleepus 6
Sleepus 7
Sleepus 8
Sleepus 9
Sleepus 10
Interruptus 2
Interruptus 3
Interruptus 4
Interruptus 5

All of our Sleepus message output has no latency. The problem is that the call to sleep doesn't actually rest the current thread, it just generates a value that implements Future, and then when we commit to the final implementation, we know that there is a delay. But because we simply ignore Future, we don't actually take advantage of latency.

To understand what the. await syntax does, let's use the Future value directly to implement our function. First, you don't use async blocks.

5. Rule asynchronous code without async keyword

If we lose the async code block, it looks like this:

fn sleepus() -> impl std::future::Future<Output=()> {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
}

The following errors occur when compiling:

error[E0277]: the trait bound `(): std::future::Future` is not satisfied
 --> src/main.rs:4:17
  |
4 | fn sleepus() -> impl std::future::Future<Output=()> {
  |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::future::Future` is not implemented for `()`
  |

The above error is because the result type of the for loop is (), which does not implement the Future trail. One way to fix this is to add a sentence after the for loop to return it to the implementation type of Future. We already know that we can use this: sleep:

fn sleepus() -> impl std::future::Future<Output=()> {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
    sleep(Duration::from_millis(0))
}

Now we will still see the warning message of unused Future value in for loop, but the error of return value has been solved. This sleep call actually does nothing. We can replace it with a real placeholder Future:

fn sleepus() -> impl std::future::Future<Output=()> {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
    async_std::future::ready(())
}

6. Realize your own Future

In order to break the sand pot, let's go one step further. Async is not applicable_ The ready function in STD library defines its own structure to realize Future. Let's call it nothing.

use std::future::Future;

struct DoNothing;

fn sleepus() -> impl Future<Output=()> {
    for i in 1..=10 {
        println!("Sleepus {}", i);
        sleep(Duration::from_millis(500));
    }
    DoNothing
}

The problem is that nothing has not yet provided a Future implementation. We'll do some compiler driven development next, and let rustc tell us how to fix this program. The first error message is:

the trait bound `DoNothing: std::future::Future` is not satisfied

So let's supplement the implementation of this trait:

impl Future for DoNothing {
}

Continue to report errors:

error[E0046]: not all trait items implemented, missing: `Output`, `poll`
 --> src/main.rs:7:1
  |
7 | impl Future for DoNothing {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^ missing `Output`, `poll` in implementation
  |
  = note: `Output` from trait: `type Output;`
  = note: `poll` from trait: `fn(std::pin::Pin<&mut Self>, &mut std::task::Context<'_>) -> std::task::Poll<<Self as std::future::Future>::Output>`

We don't really know pin < & mut self > or Context yet, but we know Output. Because we went back () and now let's do it.

use std::pin::Pin;
use std::task::{Context, Poll};

impl Future for DoNothing {
    type Output = ();

    fn poll(self: Pin<&mut Self>, ctx: &mut Context) -> Poll<Self::Output> {
        unimplemented!()
    }
}

Oh! Compilation passed! Of course, it will fail at run time, because our unimplemented! () call:

thread 'async-std/executor' panicked at 'not yet implemented', src/main.rs:13:9

Now let's try to implement poll. We need to return a value of type poll < self:: output > or poll < () >. Let's look at the definition of Poll:

pub enum Poll<T> {
    Ready(T),
    Pending,
}

Using some basic reasoning, we can understand that Ready means "our Future has been completed, this is the output", while Pending means "not finished yet". Suppose our nothing wants to return the output of type () immediately, as follows:

fn poll(self: Pin<&mut Self>, _ctx: &mut Context) -> Poll<Self::Output> {
    Poll::Ready(())
}

congratulations! You just implemented your first Future structure!

7. async and function return value

Remember the third thing async did with functions: automatically encapsulate the resulting value as a new Future. Let's show that next.

First, simplify the definition of sleepus:

fn sleepus() -> impl Future<Output=()> {
    DoNothing
}

Compilation and operation are normal. Now switch back to the async style:

async fn sleepus() {
    DoNothing
}

An error will be reported:

error[E0271]: type mismatch resolving `<impl std::future::Future as std::future::Future>::Output == ()`
  --> src/main.rs:17:20
   |
17 | async fn sleepus() {
   |                    ^ expected struct `DoNothing`, found ()
   |
   = note: expected type `DoNothing`
              found type `()`

As you can see, when you have an async function or code block, the result is automatically encapsulated into a Future implementation object. So we need to return an impl Future < Output = nothing >. Now our type needs to be Output = ().

The processing is very simple. Just add. await after doing:

async fn sleepus() {
    DoNothing.await
}

This adds a bit of intuition to our effect on. await: it extracts the Output value from nothing. But we still don't really know how it works. Now let's implement a more complex Future Keep exploring.

Original link: Analysis of the principle of async/.await in Rust asynchronous programming (I) - Huizhi network

Posted by Imad on Thu, 18 Jun 2020 20:06:38 -0700