Demystifying Closures, Futures and async-await in Rust–Part 3: Async & Await

The Async you’ve been Awaiting for

Let’s pretend we don’t know anything and jump right in and try to write our first async function. To do this, we simply prepend the async keyword to our fn declaration:

async fn async_hello() {
    debug!("Hello, asynchronously!");
}

That’s all it takes, really!

But what happens if we try to call that function directly? In our main() let’s try writing:

fn main() {
    // ...    async_hello();
}

We get a compiler warning:

warning: unused implementer of `core::future::future::Future` that must be used
   --> src/main.rs:169:5
    |
169 |     async_hello();
    |     ^^^^^^^^^^^^^^
    |
    = note: `#[warn(unused_must_use)]` on by default
    = note: futures do nothing unless you `.await` or poll them

Rust is telling us that async_hello() is returning a Future that is unused. (You may recall we’ve seen this warning before, when we tried to simply call our returns_delayed_future() in rt.enter().)

But what happens if we try to .await it just like it says?

Now we get a compiler error, since our main() is not async:

error[E0728]: `await` is only allowed inside `async` functions and blocks
   --> src/main.rs:169:5
    |
102 | fn main() {
    |    ---- this is not `async`
...
169 |     async_hello().await;
    |     ^^^^^^^^^^^^^^^^^^^ only allowed inside `async` functions and blocks

The error mentions async blocks, let’s try wrapping that line in an async {}:

async {
    async_hello().await;
}

If that block is at the end of fn main() { ... }, we immediately run into:

error[E0308]: mismatched types
   --> src/main.rs:169:5
    |
102 |   fn main() {
    |             - expected `()` because of default return type
...
169 | /     async {
170 | |         async_hello().await;
171 | |     }
    | |     ^- help: try adding a semicolon: `;`
    | |_____|
    |       expected `()`, found opaque type

Now if we add a semicolon after the block as it says:

async {
    async_hello().await;
};

We once again get:

扫描二维码关注公众号,回复: 15428609 查看本文章
warning: unused implementer of `core::future::future::Future` that must be used
   --> src/main.rs:169:5
    |
169 | /     async {
170 | |         async_hello().await;
171 | |     };
    | |______^
    |
    = note: `#[warn(unused_must_use)]` on by default
    = note: futures do nothing unless you `.await` or poll them

But this time, at the end of the async block.

We’re just running around in circles!

async -> Future

The key thing to remember is that whether we use async for a block or a function, both times we get a Future.

We already know what to do from Part 2 when we tried to call a function that returns a Future directly in main().

For a Future to be executed or polled, we need to pass our future to the Tokio runtime. Thus we need to write:

let mut rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async_hello());

So we get the output we desire:

08:10:29 [DEBUG] (1) Hello, asynchronously!

Now let’s try that async block again. First we’ll print something directly in the block:

let async_block = async {
    debug!("in async_block");
};
rt.block_on(async_block);

That works as expected. We can even ‘capture’ surrounding variables (Rust has a different and specific definition of async closure though and as of this writing, it’s a feature still deemed unstable):

{
    let x = 42;
    let async_capture = async {
        debug!("in async_capture, x => {}", x);
    };
    rt.block_on(async_capture);
}

You can think of the above code as equivalent to:

{
    let x = 42;
    let async_capture = future::lazy(|_| {
        debug!("in async_capture, x => {}", x);
    });
    rt.block_on(async_capture);
}

To recap we can think of async {...} as ‘desugaring’ to future::lazy(|_| {...}). They’re not exactly the same but for practical purposes they exhibit the same behavior.

Async Functions

Similarly, we can now begin to understand async fn as merely ‘sugar’ that returns a Future<Output=T>.

That is, the following functions are practically equivalent:

async fn async_returns_i32() -> i32 {
    42
}fn returns_future_i32() -> impl Future<Output = i32> {
    future::ready(42)
}

So the way to read an async fn f() -> T is simply as being equivalent to an ordinary fn f() -> impl Future<Output = T>.

We can even return an async block as a Future:

fn returns_async_block_i32() -> impl Future<Output = i32> {
    async { 42 }
}

All three above functions when called behave pretty much exactly the same way.

From Futures to Async-Await

Up until now, we’ve been calling rt.block_on() multiple times.

Suppose our code looks like this:

rt.block_on(future::lazy(|_| debug!("in rt.block_on()")));
let r0 = rt.block_on(future::ready("Hello from rt.block_on()"));
debug!("{}", r0);
let r1 = rt.block_on(returns_impl_future_i32());
debug!("returns_impl_future_i32() -> {}", r1);
let r2 = rt.block_on(returns_dyn_future_i32());
debug!("returns_dyn_future_i32() -> {}", r2);
let r3 = rt.block_on(returns_future_result());
debug!("returns_future_result() -> {}", r3.unwrap());
let r4 = rt.block_on(returns_future_result_dyn_error());
debug!("returns_future_result_dyn_error() -> {}", r4.unwrap());
let r5 = rt.block_on(returns_delayed_future()));
debug!("returns_delayed_future() -> {}", r5))
let r6 = rt.block_on(wait_a_sec(future::ready(42)));
debug!("wait_a_sec(future::ready(42)) -> {}", r6));
rt.block_on(async_hello());
let async_block = async {
    debug!("in async_block");
};
rt.block_on(async_block);
let x = 42;
let async_capture = async {
    debug!("in async_capture, x => {}", x);
};
rt.block_on(async_capture);
let r7 = rt.block_on(async_returns_i32());
debug!("async_returns_i32 -> {}", r7);
let r8 = rt.block_on(returns_future_i32());
debug!("returns_future_i32 -> {}", r8);
let r9 = rt.block_on(returns_async_block_i32());
debug!("returns_async_block_i32 -> {}", r9);

We can rewrite all the above using future combinators to return a single future chainor we can now truly start making use of the power and convenience async and await provide us.

As we’ve seen, an async {} and a future::lazy(|_| {…}) are practically interchangeable.

So first, we need to surround all those lines starting with future::lazy in a single async {...} block which we will block_on().

We then simply replace all the inner calls to rt.block_on(f) with f.await

The above code now becomes:

rt.block_on(
    async {
        debug!("in rt.block_on()");
        let r0 = future::ready("Hello from rt.block_on()").await;
        debug!("{}", r0);
        let r1 = returns_impl_future_i32().await;
        debug!("returns_impl_future_i32() -> {}", r1);
        let r2 = returns_dyn_future_i32().await;
        debug!("returns_dyn_future_i32() -> {}", r2);
        let r3 = returns_future_result().await;
        debug!("returns_future_result() -> {}", r3.unwrap());
        let r4 = returns_future_result_dyn_error().await;
        debug!("returns_future_result_dyn_error() -> {}", r4.unwrap());
        let r5 = returns_delayed_future().await;
        debug!("returns_delayed_future() -> {}", r5);
        let r6 = wait_a_sec(future::ready(42)).await;
        debug!("wait_a_sec(future::ready(42)) -> {}", r6);
        async_hello().await;
        let async_block = async {
            debug!("in async_block");
        };
        async_block.await;
        let x = 42;
        let async_capture = async {
            debug!("in async_capture, x => {}", x);
        };
        async_capture.await;
        let r7 = async_returns_i32().await;
        debug!("async_returns_i32 -> {}", r7);
        let r8 = returns_future_i32().await;
        debug!("returns_future_i32 -> {}", r8);
        let r9 = returns_async_block_i32().await;
        debug!("returns_async_block_i32 -> {}", r9);
    });

Like rt.block_on().await works just as well whether for a Future or an async block or an async fn.

#[tokio::main]

Now if all our main() function does is equivalent to the following:

fn main() {
    let mut rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        // do async stuff
    });
}

Then Tokio lets us pull another trick to further simplify our code and make it as if Rust had an asynchronous runtime built-in.

It uses a custom Procedural Macro that allows us to attach the #[tokio::main] attribute macro to our main() function, which lets Tokio ‘inject’ all the boilerplate code into the resulting binary.

This is how Tokio lets us simply rewrite the above as:

#[tokio::main]
async fn main() {
    // do async stuff
}

Which is now how most contemporary examples of Rust asynchronous code starts.

Async Error Handling

As with Futures, we also need to handle errors gracefully. Fortunately, Rust already provides us with a way to reduce boilerplate via the ? operator for error handling, which works just as well with .awaited Results.

That is, suppose we have a function that may return an error:

fn fallible() -> Result<(), Box<dyn Error>> {
    let _f = std::fs::File::open("foo.txt")?;
    Ok(())
}

We can call it directly from main() as such:

let _ = fallible().unwrap();

If we don’t care much for graceful error handling (we should, but this is just an exercise), we can avoid unwrapping by also changing main() to return a Result<(), Box<dyn Error>>:

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // ...    let _ = fallible()?;    Ok(())
}

Now we can easily turn fallible asynchronous, and we can just as easily .await?:

async fn fallible() -> Result<(), Box<dyn Error>> {
    let _f = std::fs::File::open("foo.txt")?;
    Ok(())
}#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // ...
    let _ = fallible().await?;
    Ok(())
}

The key to error handling in Rust is being able to make sense of both the Result success and error types.

猜你喜欢

转载自blog.csdn.net/love906897406/article/details/125901555