Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graceful exit for background tasks like in tokio #832

Closed
cecton opened this issue Jul 10, 2020 · 4 comments
Closed

Graceful exit for background tasks like in tokio #832

cecton opened this issue Jul 10, 2020 · 4 comments

Comments

@cecton
Copy link

cecton commented Jul 10, 2020

Issue

When using async-std's block_on, the background tasks spawned are neither stopped neither awaited properly. This means that destructors are not called as they should.

I talk about this with @yoshuawuyts and there seems to be a way to do it with task cancellation but I think async-std should actually stop the tasks gracefully.

Current behavior

To help understand the issue I made this repository to demonstrate: https://github.com/cecton/asyncstd-test-drops

In this example a task is ran in background for async-std and then I use block_on on a sleep. The sleep finishes before the task actually completes and we exit the block_on despite things are still running in background.

Right after in the same example I do the exact same thing with tokio and you can see that in tokio the block_on only exit when the task and its objects are dropped properly.

Here is the output of the program if I comment the tokio example and keep only the async-std example. Note that the destructor is never called:

     Running `target/debug/asyncstd-test-drops`
test 1 created
1
2
[Finished running]

The program finished in 2 seconds because the timeout is of 2 seconds. All the blocking tasks in background and objects are just not dropped.

Here is the output of the program if I comment the async-std example and keep only the tokio example. Note that the task is dropped before reaching the marker "2":

     Running `target/debug/asyncstd-test-drops`
test 2 created
1
Dropping 2...
Dropped 2.
2
[Finished running]

The program finished in 7 seconds. This is the addition of the 2 blocking codes (std::thread::sleep). It blocks the thread and prevents tokio to finish.

Now another interesting thing is this: if I keep both examples enabled, this is the output I get:

     Running `target/debug/asyncstd-test-drops`
test 1 created
1
2
test 2 created
1
Dropping 2...
test 1 dropped
Dropping 1...
Dropped 2.
2
[Finished running]

The async-std task was still running in background and it finished while the tokio test was running. Here the Test(1) object created by the async-std task is beginning the drop but it couldn't finish because the tokio drop finished before. I first thought that the background tasks were abruptly killed by async-std but this proves this is not the case. The background tasks are still running, they're just not managed anymore.

Expected behavior

I have worked a lot with asyncio on Python and recently a bit with tokio. I expect that the async runtime will drop things gracefully. You could argue that I could implement a shutdown mechanism (which is true) but that doesn't allow the runtime to simply forget about my objects and not call their destructors.

The best behavior is to wait for the background tasks to get into an awaiting state (waiting for io, waiting for a sleep, etc...) and cancel the task, deleting it and its objects. When everything has been deleted, the block_on should finish and the program can safely exit.

@yoshuawuyts
Copy link
Contributor

Hi @cecton, thanks for opening this. This issue is not unlike #830 which also had questions about the async_std::task spawn model. The resolution for that issue is that we should add the following to our docs:

The join handle will implicitly detach the child task upon being dropped. In this case, the child task may outlive the parent [...]

This behavior has been a deliberate choice in async-std to create parity between std::thread::Thread and async_std::task::Task. As a result of this our tasks can be run to completion by any executor and runtime, which we think is really nice for compatibility between runtimes. If our task::block_on was somehow special we would deviate from the std::thread behavior, and lose these benefits.

I don't think this is a change we can make at this time. Instead I would recommend trying to call Task::join / Task::cancel, or creating a synchronization construct for detached tasks using event-listener or async-channel.

@cecton
Copy link
Author

cecton commented Jul 13, 2020

And there is no way to do a graceful shutdown without the joinhandle? Or at least gracefully exit when the process exit?

I think it is weird to allow the program to go into an inconsistent state where destructors are ignored.

@yoshuawuyts
Copy link
Contributor

I think it is weird to allow the program to go into an inconsistent state where destructors are ignored.

Rust doesn't guarantee for destructors to ever run; on a power outage or process reap there is no way these can be run anyway so it's a case to always account for.

Or at least gracefully exit when the process exit?

Good question: Boats wrote a post on async drop which would allow for "graceful exit on drop". But that wouldn't apply for Tasks since the background behavior is the way it is by design.

Instead we may look in a different direction, and aim to make joining multiple tasks easy. I did some work on this through parallel-stream (though wouldn't recommend using it yet). Another option would be an async version of rayon::scope though nobody has figured out how to make this work for async Rust yet. It's also possible to hand-roll constructs using task::spawn and FuturesUnordered / futures::FuturesExt::for_each_concurrent.

@cecton
Copy link
Author

cecton commented Jul 14, 2020

I just tested with the for_each_concurrent approach and it works pretty neat. Thanks a lot! I think I will use that for now.

fn wait_completion()
-> (mpsc::UnboundedSender<Pin<Box<dyn Future<Output = ()> + Send>>>, Pin<Box<dyn Future<Output = ()> + Send>>)
{
    let (tx, rx) = mpsc::unbounded::<Pin<Box<dyn Future<Output = ()> + Send>>>();
    let monitor = rx.for_each_concurrent(None, |x| async move { x.await; } );

    (tx, Box::pin(monitor))
}

...

    let (mut tx, monitor) = wait_completion();
    let completion = async_std::task::spawn(monitor);
    ...
    tx.send(async_std::task::spawn(some_task())).unwrap();
    ...
    // graceful shutdown
    tx.close_channel();
    completion.await;

(This code doesn't include the exit signal handling, only the clean shutdown)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants