A few doubts on using tokio

#1

Hi! I am trying to learn about using tokio. But now I am stuck in understanding spawning and tasks. What exactly is a task and how is it different from a future? The docs mention that the tasks are like “asynchronous goroutines”? But the analogu doesn’t make any sense to me. Please hep me here.

And the second one is the lazy function. According to the docs,

Creates a new future which will eventually be the same as the one created by the closure provided.

The provided closure is only run once the future has a callback scheduled on it, otherwise the callback never runs. Once run, however, this future is the same as the one the closure creates.

What does callback refer to here? What is meant by the “future from the closure” and “future created from the lazy function”? What is meant by them eventually becoming one?
Please explain this with some example.

tokio::run(lazy(|| {
    let (tx, rx) = oneshot::channel();

    tokio::spawn(lazy(|| {
        tx.send("hello from spawned task");
        Ok(())
    }));

    rx.and_then(|msg| {
        println!("Got `{}`", msg);
        Ok(())
    })
    .map_err(|e| println!("error = {:?}", e))
}));

Here in the above example what is the use of Ok(())? Shouldn’t it be something like Ok(Async::Result(())), if the closure gives rise to the future?

A reddit post for the same.

#2

Is this reddit thread also yours? You should mention it, not because I think there’s something wrong with it, but so people can see what answers are there and maybe refer to that.

1 Like
#3

Okay.

#4

Not a Tokio expert, but maybe I can help a bit.

A task is a future that returns (). This means you have to do something extra if you want to get a value out of the task.

The lazy function takes a closure which returns a future. The ‘callback’ referenced here is the closure (the argument to the lazy function.) lazy will take that callback and schedule it to be run at a later time. When it is called, it should return a future, which will likewise be scheduled to run. Thus the lazy future will ‘become’ the future returned by ts argument.

#5

First of all, the concept of tasks and futures are not tokio specific. The futures_api has just been stabilized and std now has Task and Future as well.

I think you can most easily imagine, when comparing to more synchronous code, a task would be like a thread and futures like function calls.

When you are in async context, the end result of your async block or your async function will be a task. It will also be a future, albeit a somewhat special future, just like fn main() is a somewhat special function. It will be the top most future.

However inside, you can await other futures, and they can await futures again, and you can combine them, like with select or join and so forth, so in your task there will be like a whole tree of futures. The task conceptually ties all of these together.

So if the innermost future is an IO operation, all futures in the task, are blocked waiting for IO to happen. So, none of them can do any work. That’s why for execution, we reason about tasks, not futures.

When IO comes in, the reactor wakes the task, not the future. The executor will now poll the topmost future in the task, which through .await will poll the next one etc all the way down to the IO one, which will now be able to do some work and might now return Poll::Ready instead of Poll::Pending.

Thus when you spawn a future, it get’s a new task and it becomes the topmost future of that task. It can now run concurrently compared to the current execution context. It will not block the current task as await does. So, it’s a bit like spawning a separate thread, only more lightweight.

As a consequence, the call doesn’t really return with the result of the computation, hence most spawn methods will require you to return (). You can still get the result with a feature called remote_handle from the futures library, which is a little bit like thread.join.

I hope that makes more sense now.

4 Likes