Joined futures run sequentially

I'm new to async code in Rust. The code below starts 2 futures, one slow and one fast. The slow future is made slow because it calculates the factors of a large number. Each future can be polled 10 times before it becomes ready.

I'd expect the fast slow to finish all 10 iterations while the slow future takes its time to do its work. However, the 2 futures seem to alternate.

What exactly is the behavior of join!?

use factor::factor::factor;
use futures::executor::block_on;
use futures::join;
use futures::task::{Context, Poll};
use futures::Future;
use std::pin::Pin;

#[derive(Debug)]
struct Task {
    pub name: String,
    pub num: u32,
    pub count: u32,
}

impl Task {
    pub fn new(name: String, num: u32) -> Self {
        Self {
            name,
            num,
            count: 0,
        }
    }
}

impl Future for Task {
    type Output = ();

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.count > 10 {
            Poll::Ready(())
        } else {
            factor(self.num as i64);
            println!("The {} task progress {}", &self.name, self.count);
            self.get_mut().count += 1;
            cx.waker().clone().wake();

            Poll::Pending
        }
    }
}

async fn async_op() {
    let fast_task = Task::new("fast".to_string(), 123);
    let slow_task = Task::new("slow".to_string(), 43213200);
    join!(fast_task, slow_task);
}

fn main() {
    block_on(async_op());
}

When you run all the code on one thread, futures will interleave somehow in any case, since only one of them can run at any single moment. The exact order of execution might be different, but, since you wake the future immediately after it is polled, executor is free to poll it again as fast as it wants, and polling all existing (and woken) futures in order is probably the most robust strategy, ensuring that every task will eventually get its time, unless some of them is misbehaving (and blocks the thread).

1 Like

Every time the async function with the join! is polled, it should proceed to poll both halves, one after the other. This is why you are recommended to not block the thread.

The poll! macro does not introduce threads, so it will not poll them in parallel.

You are right. The key thing here is that there is only one thread. If the futures themselves run on their own threads or a thread pool, separate from the executor thread, I bet the executor will be able to poll both futures as fast as it can. That way, the fast future will finish all of its iterations fast. I'll see if I can change the code to do that.

This is possible, but not with join!. You have to spawn them as separate top-level tasks on the executor. Generally the mistake here is that it takes a long time for your functions to return from poll: Futures are good at waiting for a lot of things (by immediately returning Pending when the thing is not ready), not at doing a lot of things (by taking a long time to return from poll).

3 Likes

You have to spawn them as separate top-level tasks on the executor makes total sense to me!