If I have an async block and it is interrupted, does program execution continue after the block?

Hello,

I am following the Rust book and think nowhere have I seen an answer to this question:
they give an improvement to the problem that if the below code is wrapped into only one outer async loop then the second while let loop is executed only after the first loop ends.
To me the fact that this wrapping in 2 async blocks solves this problem implies that program execution continues after the async block which is stopped and resumed at a later time.
Am I right? I gave a short code example of how I assume the execution flow to work.

let tx_fut = async {
            let vals = vec![
                String::from("hi"),
                String::from("from"),
                String::from("the"),
                String::from("future"),
            ];

            for val in vals {
                tx.send(val).unwrap();
                trpl::sleep(Duration::from_millis(500)).await;
            }
        };

        let rx_fut = async {
            while let Some(value) = rx.recv().await {
                println!("received '{value}'");
            }
        };

        trpl::join(tx_fut, rx_fut).await;
async {
// ...
// execution stops here
foo().await;
// ...
}

// and immediately continues here
let x = 5;
continue(x);
//...

In Rust—contrary to other prominent languages like JS or C#, for example—a future does nothing unless it is polled. I.e. futures in Rust are lazy and not eager. If you create a future like let x = async { 0 };, the future is not going to be executed, unless you x.await; it.[1]

In your example,

is the line that starts polling tx_fut and rx_fut concurrently. Before, neither tx_fut nor rx_fut will do anything.


  1. Some runtimes like Tokio have a concept called tasks, which are futures that are immediately polled by the runtime, without you needing to .await the task. ↩︎

2 Likes

sorry I made a mistake when reading the code.
I had this question: execution stops when we call await on future returned by foo().
Does execution continue at let x = 5 after the block while we wait for the future to be availabe? if not what happens? does a different thread or even process get scheduled depending on the scheduler?

async {
// ...
// execution stops here
foo().await;
// ...
}

// and immediately continues here
let x = 5;
continue(x);
//...

Also, what is the benefit of a future if execution can only progess within its async block but not further? It seems like a small difference over busy waiting.

The code as is never executes the Future produced by the async block, so it will never reach the foo().await.

To properly answer what happens at foo().await you need to know how you're executing that Future and what foo() does.

yes I was unsure about that. thanks.

Without proper executor all you may get is busy-waiting. Executor and executor-provided low-level functions (like AsyncRead and AsyncWrite work in tandem): Executor starts your coroutine and when your coroutine “calls” AsyncRead or AsyncWrite they “tell” the Executor that your coroutine started some “interesting process” in the outside world and would like to be woken up when that process would finish.

That's why poll have Context and why Context have a Waker that's Executor-specific way to notify executor about such desire.

Most of the time monitoring thread with epoll is used to schedule tasks in that model, but that's up to the Executor to decide how asyncronous events should be handled.

1 Like

thanks!

this is an updated version of my question:

let fut1 = async {
foo().await;
bar().await;
}
trpl::run(fut1);

// if fut1 is still not done can we ever execute continue1() and continue2()?
continue1();
continue2();
//...

That depends on what trpl::run does. If it were to block the current thread until fut1 is completed, then no.

are there any operations that would not block in fut1 but block if called without an async block? If not, then to me it seems that futures do not have an advantage over threads in terms of continuing with the program execution. The only benefit would be that for futures you would not need to have the overhead of creating threads and context switching.

Network I/O and channels' recv are examples of operations that when implemented as Futures do not block the current thread.

okay so there are operations that would normally block if used in a thread (do you know why?) but not block when executed through a future. Then futures can make progress on the execution of a program where threads could not. Correct?

Simply because the only way for a thread to "wait" is to block, while a Future can "suspend" itself while it waits, allowing something else to happen in that time.

Title of your post, and rest of your post ask slightly different questions.

Does program execution inside the async block continue after .await?

Usually, but not always.

Code after any .await is not guaranteed to resume. It's possible that execution will be terminated completely at any .await point (not suspended, but completely gone, destroying all of the state in the async block, with no way to resume). This generally needs to be set up explicitly, e.g. by executing the async block with a timeout.

Does program execution outside of async block continue after the async block?

Yes.

There's no relationship between what happens inside an async block and the flow of the code outside that defines it. async block is like defining a function or closure. The source code existing doesn't make the code run. It doesn't block anything around it. There's no jumping in or out, there's no stopping or resuming. The async code isn't doing anything.

The async code starts running only once it's used as a Future, and the Future::poll() method gets called. .await connects different futures together, making it possible to get them polled when their turn comes, but .await is passive, and by itself it doesn't run code. It only passes poll() down to the other futures, when polled code reaches the .await point.

Polling doesn't happen automatically. It needs to be done explicitly, and how that is done depends on the async runtime you use. If you have something like block_on(future), then the caller will be blocked until polling of the Future is done completely. Or it can be something like spawn(future) which will make the async code polled somewhere else, and it won't affect the the caller.

In typical setups you don't have any way to pause and resume a Future. It either runs completely or not. Polling and waking is done behind the scenes by the executor, and it's abstracted away.

Interaction with async code via async channels is indirect. If the async code has been spawned, polled, and happens to be waiting on a channel, then it will continue when the channel receives the message, but this happens asynchronously, and it's not causing execution jumping into the async block.

1 Like

No, no, no. They may be called in non-blocking fashion. recv have a O_NONBLOCK version that can be called in a normal thread, too. The question is: what can you do if there are no more data to consume?

Without epoll and async machinery you would just be sitting there, in a tight loop, making CPU hot.

No, you need another ingridient to make the whole thing fly. epoll allows you to specify many “fds of interes”, kernel would sleep and wait till one of them would be “ready to act” and when one of them is ready to be processed – you can call the exact same recv… but now with high chance of getting some useful data from it[1].

Without kernel giving some asynchronous API that permits us to sleep till “something interesting” may happen the whole async machinery would be an excercise in futility.


  1. Still not guaranteed, BTW, spurious wakeups are allowed. ↩︎

Go read the Book. It's honestly understandable that we at URLO don't always recall the entirety of our learning materials, but a bit of searching wouldn't hurt here.

On the other hand,

For anyone asking for help, I recommend linking the relevant sections of any tutorials you're excerpting code from.

Ranting to the Book

This is why I think we shouldn't have done the re-export dance. It serves only to obfuscate the underlying library functions. Granted, the macros used for futures::future::join wouldn't have let most people get to the bottom of it; That'd have to wait until rustdoc is integrated with cargo expand.

Now to clear your confusion, I must emphasize that async is not magic. It doesn't take care of control flow for you: It merely cede execution to Future, and to whoever you hand it out.

Naturally, you hand it out to a runtime, more specifically an executor. And all the cleverness goes inside that, among them Context and Waker. But you don't need to learn about wakeups to understand async. In your example, trpl::join(futures::future::join) is responsible for concurrency, and that doesn't rely on executor specifics. That could've ran just fine, even on a "trivial" executor with no wakeup machinery!