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:
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 resultPoll::Pending
: the future has not been done, before returning the functionpoll
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
returnsPoll::Pending
, the future yields to the executor and the executor won't run thepoll
again until thewoke
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.