A2: Poll and advanced async

In this appendix we are going over the more advanced parts of async/await in Rust. For the most part, as a consumer of async/await, you normally do not have to deal with this. However, in an embedded context, it may become necessary to interact with polling interfaces which do not have any knowledge of the async/await features in Rust. This appendix will present a short introduction to the topic such that you can work with it if needed.

Futures in Rust

Async/await in Rust is syntactic sugar over the Future-trait. Futures provide a polling mechanism to allow them to make progress. Futures on their own, do not actually run. They need an executor to poll them iteratively.

A very simplified mental version of such an executor could be:

pub struct Executor {
    futures: Vec<Box<dyn Future>>,
}

impl Executor {
    pub fn run(&mut self) {
        for future in self.futures.iter_mut() {
            future.poll();
        }
    }
}

The previous code sample already shows some interesting bits about async/await in Rust. First and foremost, they are not threads. Some executors like the one implemented in tokio-rs allow for futures to be scheduled on multiple threads, but you do not get out-of-the-box actual multi-threading. As an example of this, the executor we used in this course, only uses 1 core. Where futures shine the most, is in applications where you need to wait a lot on external events like reading data from disk, making a network request or waiting for a button to be pressed.

However, the above example has a big issue: futures might need to have references to itself. This is not easy in Rust, as moving values in Rust is an operation that is often done. When compiling a Rust program (without optimizations) these moves often get translated into a memcpy. If you have references to yourself, and you move this memory, the references will still reference the old memory location. This would result in immediate undefined behavior which is not something you would want in a Rust program.

Normally, Rust does not allow direct self-references, due to the exact issue described above. To still allow futures to make self-references, the Pin type can be used. The Pin type takes a mutable pointer to a memory location and ensures that the memory behind it does not get moved. To ensure that futures never get moved while they are executing, the polling method requires that the future is pinned.

However, pinning every future significantly increases the complexity of calling futures, even in scenarios where there are no self-references. To combat this, Rust defines the UnPin trait. If a struct implements the UnPin trait, pinnig will become a NOP. Like Send and Sync, UnPin is a so-called auto-trait and is automatically implemented on all your types if all its fields are also UnPin.

At this point, we have an executor that allows its futures to have self-references, but constantly polling might be a waste of resources. An example of this would be an embedded device with limited computing/memory/energy resources that needs to wait on a timer to go off, and keeps constantly polling for updates. This is why Rust's Future::poll needs a context as its argument. This context allows to get access to a Waker. This Waker can then be used to signal to the executor that it is ready to make progress. This mechanism then allows the executor to prioritize futures that actually can make progress while allowing it to go into a sleep mode if no future can make progress.

Interacting with polling interfaces

When writing async code with the async/await keywords, you might come in the situation where you need to interact with a polling interface. Some of these polling interfaces, require a context which is normally not available when using the async keyword. For this, Rust provides the core::future::poll_fn function. As the argument, you can pass it a function that takes a context as its argument. This allows you inside this function to access the current context and call the polling interface.

use std::future::Future;
use std::pin::pin;
use std::time::Duration;
use tokio::time::sleep;

async fn sleepy_polling_future() {
    sleep(Duration::from_millis(100)).await;
    println!("100 ms have elapsed");
}

#[tokio::main]
async fn main() {
    let mut future = pin!(sleepy_polling_future()); // first we pin
    std::future::poll_fn(|cx| future.as_mut().poll(cx)).await; // Then we execute
}

Further reading

Here are some interesting resources that may be off help when further studying the core concepts of advanced async Rust.