-
Notifications
You must be signed in to change notification settings - Fork 341
Deadlock with recursive task::block_on #644
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
Comments
I believe this should work with the new scheduler: #631 Can you try running the test again with |
@stjepang Absolutely! Works in the |
Still there is some problem with recursive block_on. It says in the documentation that
But it is not true. use async_std::{stream, sync, task};
use futures::select;
use futures::{FutureExt, StreamExt};
use std::time::Duration;
fn main() {
let (close_tx, close_rx) = sync::channel::<()>(1);
task::spawn(async move {
task::sleep(Duration::from_secs(1)).await;
let _tx = close_tx;
println!("close_tx should be dropped");
});
task::block_on(async move {
let mut close_rx = close_rx.fuse();
select! {
_ = work().fuse() => (),
_ = close_rx.next() => (),
}
});
}
async fn work() {
let interval1 = stream::interval(Duration::from_millis(3000));
let interval2 = stream::interval(Duration::from_millis(2000));
// task::block_on(async move {
// let mut interval1 = interval1.fuse();
// let mut interval2 = interval2.fuse();
// loop {
// select! {
// _ = interval1.next() => {
// println!("interval1");
// },
// _ = interval2.next() => {
// println!("interval2");
// },
// }
// }
// });
task::spawn(async move {
let mut interval1 = interval1.fuse();
let mut interval2 = interval2.fuse();
loop {
select! {
_ = interval1.next() => {
println!("interval1");
},
_ = interval2.next() => {
println!("interval2");
},
}
}
})
.await;
} In the code above, if we replace the spawn-then-await with block_on, the program will never exit, which is true for both the new scheduler and the old one. In my use case, I have to use block_on because the Future I am dealing with is non-Send. |
@kylerky I think your example could be simplified as: #[async_std::test]
async fn block_on() {
use async_std::{task, future::{timeout, pending}};
use std::time::Duration;
task::block_on(async {
let _ = timeout(
Duration::from_secs(1),
async {
task::block_on(pending::<()>())
}
).await;
});
} The reason it never finishes it because the inner std::thread::spawn(|| {
loop {}
}).join(); Once the above statement runs, both the spawned thread, and the parent thread can't be stopped unless the whole process itself aborts or exits. See how to terminate or suspend a rust thread. As suggested in the link, you could use (async variant of) channels or other communication to signal the timeout to the inner task, to make it exit. An issue with the current scheduler is that it uses a |
@rmanoka Thanks for your explanation. I find it a surprise that tasks spawned by Also, the problem is that |
Since |
By being stolen, I do no mean offloading the task to a different thread. In the However, in the recursive |
Is this still true? My mental model is that But if I understand what you're saying correctly, It seems like a quite severe restriction that should at the very least be clearly documented. |
It's not true anymore. More info: smol-rs/smol#177 (comment) |
@algesten In the latest release (1.6.3), async-std no longer accepts the above example and panics if It would be nice to document the panic behaviour, if it is to be expected though. |
@rmanoka smol on the other hand works perfectly on one cpu with same block_on/spawn/block_on recursion, even with 4 smol threads. perhaps it would lock up on deeper recursion with a bunch of heavy tasks, i dunno. moreover, smol has an unbounded sync channel with try_send, so you don't even need to call block_on inside other tasks, because you can just communicate from sync context into async normally. i'm jumping async_std for smol, personally. |
@installgentoo This is fantastic! The use-case for block_on (possibly inside a task) comes from the async-scoped crate, where unfortunately, the only safe abstraction we could think of was to I'm closing this as the original issue has been solved. Shout out to the authors of this and the |
@rmanoka By the way, |
@stjepang this is awesome! Just clarifying: to spawn a |
Yes, exactly - you can think of This design makes it possible to do lots of interesting things - here's a scoped executor with task priorities: |
That is a very elegant abstraction! |
When calling
task::block_on
recursively, the executor seems to dead-lock even when the recursion depth is much smaller than num cpus.Sample code (deadlocks on a 8-cpu machine):
It seems that the test should deadlock when input >= num_cpus, but even when input=2, on a 8-cpu machine this seems to deadlock. Is this expected behaviour? Interestingly, input = 1 (which does involve a recursive call) does not deadlock. If the
block_on
is removed, the test indeed passes for large input values.Aside: it would be great if the executor could detect
block_on
called within the pool processor threads, and spawn more threads. Theblock_on
could be considered an explicit hint that the particular worker is probably going to block.The text was updated successfully, but these errors were encountered: