Search code examples
asynchronousrustrust-futures

Running rust async futures containing infinite loops


Provided this minimal example:

use futures::future::join;
use futures::executor::block_on;

async fn func_a() {
    loop {
        println!("Function A is running");
        // Simulate some work
    }
}

async fn func_b() {
    loop {
        println!("Function B is running");
        // Simulate some work
    }
}

fn main() {
    let join_handle = join(func_a(), func_b());
    block_on(join_handle);
}

While the application I'm working on is a little more complex, the above example represents what I'm trying to do.

I have two functions A and B. While trying to run them concurrently as futures (instead of threads), it seems every method I use, produces only this output:

...
Function A is running
Function A is running
Function A is running
Function A is running
Function A is running
...

If I add a return within func_a then func_b is executed after func_a returns.

Reading about futures as well as async, it seems either the joining and blocking in the code, should attempt to execute both functions simultaneously, however neither the example above, nor join! macro produce this simultaneous behavior.

What could I possibly be doing wrong?


Solution

  • Asynchronous multitasking (the terminology varies from source to source, here I mean what Rust means with async/await) is not preemptive, meaning that the runtime that is supposed to schedule several task will never interrupt a task that is running to launch an other task; rather, each task is supposed to signal the runtime that, at this precise instant, it can pause the current task to do something else.

    In Rust, this happens each time you use .await on a Future. In your code, you never use this, so the first task that is executed (here, it's func_a()) will simply never be interrupted by the runtime.

    Usually, you only .await when you need to explicitly "wait" for some other resource to be available, that is, there is an other task that is supposed to produce the resource you need, so you .await to allow other stuff to happen until that task has been scheduled, it has ended, and the resource produced. In your specific example, though, you only want to .await to signal the runtime that it can pause the current task. To this end, tokio provides the yield_now dummy task:

    fn func_a() {
        loop {
            println!("Function A is running");
            tokio::task::yield_now().await;
        }
    }
    

    Note that there is no guarantee that tokio will schedule something else when you yield. Morally, you are just offering the runtime to decide what to do at that point, but it may very well decide to keep running the same task.