Futures spawned with tokio::spawn inside of #[tokio::main] async fn main() {} are never run if main() returns first:
#[tokio::main]
async fn main() {
tokio::spawn(async {
println!("never printed");
}
}
This is because the expansion of #[tokio::main] only calls .block_on() and doesn't wait for the runtime to become idle after it returns: https://github.com/tokio-rs/tokio/blob/master/tokio-macros/src/lib.rs#L101-L119
I think this is worth discussing because there's conflicting precedents here:
std::thread::spawn() API, by default spawned threads don't keep the process around unless you explicitly join on them; however, that's not currently possible with tokio::spawn(). You have to wire up the joining yourself somehow.At the very least, I would accept an option passed to the attribute itself, e.g. #[tokio::main(shutdown_on_idle = true)].
@abonander this cannot be default behavior for obvious reason: some may spawn future that gets forever unresolved until some condition is met, meaning your main forever stuck if you're not careful.
Making it configurable through attribute is fine I guess, it is only code gen
In the async world, Node.js by default waits for the event loop to become idle before exiting.
Not the argument
@abonander I think the key here would be to provide a type of join handle for spawn.
The other idea is to provide an easy to use TaskGroup type of thing that allows you to perform graceful shutdowns which imo is a better pattern to push people too.
I've started to play around with this idea here https://github.com/LucioFranco/iquit/
The provided example doesn't properly highlight the actual problem. To solve the concrete problem highlighted by the example above, all that has to be done is to replace the call to spawn with .await and the text is printed.
To understand the actual problem, we need at least 2 asynchronous computations, that we'd like to .await at the same time. For that, we have to expand the original example:
#[tokio::main]
async fn main() {
tokio::spawn(async {
println!("never printed");
});
tokio::spawn(async {
println!("never printed");
});
}
In this case, we probably do not want to use .await, because it would cause the second computation to never start before the first one has finished, even if they're completely independent from one another.
While Runtime::shutdown_on_idle works for this specific case, what happens, if those async. computations return a result? AFAIK, there is no way to retrieve the result from an async. computation, that has been put onto the run queue with spawn. A JoinHandle as proposed by @LucioFranco would certainly help in this case, because it carries the result from the computation just like it does for threads and it allows for both computations to be started before waiting for the result, thus taking advantage of asynchronous programming. In conclusion, we could even get rid of shutdown_on_idle, because we'd have the tools to specify for which tasks we'd like to wait and for which we don't.
The final solution might look something like this:
#[tokio::main]
async fn main() {
let handle0 = tokio::spawn(async {
println!("never printed");
});
let handle1 = tokio::spawn(async {
println!("never printed");
});
handle0.await;
handle1.await;
}
P.S.: The example is missing a ); after async { [...] }.
Looks like the conclusion is to explore waiting on task groups to finish externally to Tokio. Then we can evaluate any proposals.
Most helpful comment
I think this is worth discussing because there's conflicting precedents here:
std::thread::spawn()API, by default spawned threads don't keep the process around unless you explicitly join on them; however, that's not currently possible withtokio::spawn(). You have to wire up the joining yourself somehow.At the very least, I would accept an option passed to the attribute itself, e.g.
#[tokio::main(shutdown_on_idle = true)].