Skip to content

Rust Async Brief

As I tried to write a demo by rust wasm, I was thinking about what should be a better signature to export by rust for wasm. In the demo, I exported the process function as a promise:

pub async fn process(input: &[u8]) -> js_sys::Promise

This blog discusses my understanding of async concept in rust and discussion whether returning a promise is a good choice.

Async in Rust

During my college time, I wrote C# a lot and there are asynchronized model as well, however, I don't know much about this concept, let alone the asynchronized model in rust. This blog is a note written down during reading rust async book.

Some examples are great to the newer but a bit confusing if you already learn some concurrency programming. For example, when introducing the simplified poll method of Future, it reduces the arguments passed through the poll which lacks the information about how the woke function and the Future structure are linked together. Another example is that during introducing poll, it says nothing about the executor, which makes it ambiguous about the question of who will trigger the poll method again after woken is called.

However, the guide is nice and explain the concept clearly with sufficient examples.

Overall

To learn async in rust, we need to learn the Future trait along with the poll and woke functions. Then, we need to know the relationship between asynchronized functions and the executor, and how the Future trait is used here.

Then, we need to learn the await keyword and then learn about the propagation of await. Finally, we should know how to separate the sync and async functions through the executor.

Future Trait, Poll and Woke

A future represents an asynchronous computation obtained by use of async.

pub trait Future {
    /// The type of value produced on completion.
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}

A future is a value that might not have finished computing yet. Futures can be advanced by calling the poll function, which will drive the future as far towards completion as possible. This implies the future is lazily executed, and we provide an example below to demonstrate it.

The poll function will be called by an executor(we will introduce it later), and return one of two possible results:

  • Poll::Ready: the future completes and could return the result
  • Poll::Pending: the future has not been done, before returning the function poll need arrange for the wake() function to be called when the Future is ready to make more progress.

The first one is easy case. The second one, when the result is pending, the future will yield to the executor, and be pending by the executor until the woke function is called. Note that before returning the pending, we need to pass the woke to the underlying resource. It's indeed a callback used to notify the caller for the callee to avoid spinning.

A common example is that a future which reads a socket, its poll method will check whether the socket is ready. If not, the poll submits its woke function to epoll, which will call the submitted callback function(woke here) once there are available data in the socket.

If the woke doesn't be called, the future will pending inside the executor forever and won't return a result as the poll won't be called again and return a ready.

The following diagram show the relationship, the woke call will notify the executor to call poll again.

      future
        | add
    executor ----> poll --if Ready--> return value
    |                              |
    |     (be called sometimes)    |
    |             woke   <--- submit woke function
    |              +               |
    |              +               |
    |              +            return Pending
    |          end pending         |
    |              +               |
    pending by executor <----------|          

Implement a Future

After learning about the Future trait, we know some key points:

  • future is lazy, it executes only if something call poll
  • future is executed by the executor
  • if the poll returns Poll::Pending, the future yields to the executor and the executor won't run the poll again until the woke is called.

Hence, we can write a simple demo to implement a future. It will return Poll::Pending for the first five times, and then return the Poll::Ready with value 5. Before returning pending, it spawns another thread to call the woke function after sleeping 1 second. The new thread is required as the woke must be called by another component(usually a resource which might pending).

fn main() {
    let f = FutureCounter { count: 0 };
    println!(
        "the final resulut of the Future Counter is: {}",
        block_on(f)
    )
}

struct FutureCounter {
    count: i8,
}

impl Future for FutureCounter {
    type Output = i8;
    fn poll(
        self: std::pin::Pin<&mut Self>,
        cx: &mut std::task::Context<'_>,
    ) -> std::task::Poll<Self::Output> {
        let f = self.get_mut();
        let c = f.count.borrow();
        if *c > 4 {
            return Poll::Ready(*c);
        }

        let waker = Some(cx.waker().clone()).unwrap();
        println!(
            "{} FutureCounter's poll method is called at {}th time",
            Local::now().format("%H:%M:%S"),
            c
        );
        register_callback(waker);
        f.count += 1;
        Poll::Pending
    }
}

/// # register_callback
///
/// it sleeps one second and then call the wake method of waker
fn register_callback(waker: Waker) {
    thread::spawn(move || {
        thread::sleep(Duration::from_secs(1));
        waker.wake();
    });
}

The console outputs the content below.

10:30:36 FutureCounter's poll method is called at 0th time
10:30:37 FutureCounter's poll method is called at 1th time
10:30:38 FutureCounter's poll method is called at 2th time
10:30:39 FutureCounter's poll method is called at 3th time
10:30:40 FutureCounter's poll method is called at 4th time
the final resulut of the Future Counter is: 5

Future Is Lazy

Future(js names it Promise) is a structure that you may get the result now, or in the future.

In rust, a Future is lazily executed until it's submitted to the executor. It means a created future won't be executed until it's submitted to an executor. From this view, it's very like a monad which represents a computation and won't be triggered until we run it.

The following code simply verifies it, and its output is: assigned after creating the future

fn main() {
  lazy_async()
}

static mut VAL:  &str= "default value";

fn lazy_async() {
  let future = demo();
  sleep_ms(1000);
  unsafe { VAL = "assigned after creating the future" };
  println!("{}", block_on(future));
}

async fn demo() -> String{
  unsafe { VAL.to_string() }
}

Async and Sync Functions

The async function and sync function are different. The async function definition could be understood as a computation constructor, and the async function call could be understood as a computation. The (future) executor is the one who runs the computation and get the results.

In this view, it's as same as the monad in haskell, which represents a computation as well. However, the async function as a computation might need to be run several times as it cannot complete at one time. This several turns of computing execution is done by the poll function. Moreover, the analogy here is in-concise as the async function doesn't satisfy the trait of the monad.

Differ from the async function, the sync function will be executed in the function call stack and block the current thread until completing. It doesn't require an executor because it does not return a future that needs to be driven to completion.

Block_on and Await

Rust futures::executor provides the block_on method. Natively, rust supports to await a future to get the result, which is only available inside a async function. This topic discusses their differences.

The executor is the bridge from the sync function into the async function. The async function call is just a computation to be called, so we still need to run it by the executor. In the sync function, if you want to call a synchronized function, you may choose block_on function which wraps an executor instead it, as the source code demonstrates:

pub fn block_on<F: Future>(f: F) -> F::Output {
    pin_mut!(f);
    run_executor(|cx| f.as_mut().poll(cx))
}

The await keyword is available in async function only, as all the async functions are managed by the executor, which could pending a chain of async functions once the underlying async function is await. On the contrary, the sync function has no idea about the action once one of the underlying async function is await because it's not managed by the executor.

Due to it, await for an async function inside a sync function is not allowed.

Exported Wasm API, Async or Sync?

Let's back to the topic, when we export process api by wasm, is the async better?

Based on the scenario, the http request is in an async way and could be yield, which is not necessary to block the whole thread. Hence, the process is better to be an async function as well, which could be yield as well once the underlying http request yield so the executor could arrange them better.

To conclude, exporting a function which returns a Promise by wasm is a good choice.