-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
add TaskPool::spawn_pollable so that the consumer does not need a blo… #4102
base: main
Are you sure you want to change the base?
Conversation
4bfe52c
to
9a2a7e8
Compare
@DJMcNab you're welcome to provide feedback :) |
6bdf4ee
to
681afa1
Compare
/// on every frame update without blocking on a future | ||
#[derive(Debug)] | ||
pub struct PollableTask<T> { | ||
receiver: Receiver<T>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is using channels actually preferable to calling future::block_on
? Has anyone run benchmarks? Will this work on wasm
? (pretty sure future::block_on
doesn't, but its still worth checking)
Are there alternatives that don't involve async channels (which will allocate and use atomics)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That looks like #2650.
I haven't run the numbers on it though. We need to get data into the ecs from outside the ecs, which means that it needs to live somewhere with a stable address so that both sides can talk to each other. Currently, that's where the Task
lives, in this PR it's in the channel internals.
Adding new items can also happen at any time relative to the system code happening, which means we definitely need atomics in some form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There were these benches from the original author, hanabi1224.
Looking at it, they probably should be ported to criterion and a bench needs to be added for the approach in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think another option is to use or implement something similar to FuturesExt::now_or_never
. It would have almost no overhead compared to using future::block_on
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can confirm the example doesn't currently compile on wasm, since the single threaded task pool doesn't have the spawn_pollable api. You might be able to get it to work on wasm by detaching the task instead of storing it and then copying the code for spawn pollable over to the single threaded task pool.
#4627 aims to be a simpler and faster alternative to this. |
I updated #2650 to also include the approach in this branch: https://github.com/hymm/bevy/blob/async-bench/examples/async_tasks/async_bench.rs
This seems to be significantly faster than the noop waker approach. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broadly I'm in agreement with this change, I think it's a lot nicer.
No concerns inherent to the solution, just some concerns around naming and similar things.
crates/bevy_tasks/src/task_pool.rs
Outdated
@@ -239,6 +241,26 @@ impl TaskPool { | |||
Task::new(self.executor.spawn(future)) | |||
} | |||
|
|||
/// Spawns a static future onto the thread pool. The returned `PollableTask` is not a future, | |||
/// but can be polled in system functions on every frame update without being blocked on | |||
pub fn spawn_pollable<T>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not a huge fan of this name (and its derivates, i.e. PollableTask
and PollableTask::poll
); normal tasks can also be polled, using Future::poll
.
Unfortunately, I don't have any concrete suggestions for a better name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not very of the name either, but I couldn't come up with an alternative, so I just took the name from the original PR.
CheckableTask
? ChannelledTask
? Simply calling it Task
, too, identifying it via the package: ...::channelled::Task
?
crates/bevy_tasks/src/task_pool.rs
Outdated
let result = future.await; | ||
match sender.send(result).await { | ||
Ok(_) => {} | ||
Err(_) => warn!("Could not send result of task to receiver"), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error message could/should be expanded. E.g.
Err(_) => warn!("Could not send result of task to receiver"), | |
Err(_) => error!("Sending result for future {future_name} (`Future<Output={return_name}>`) failed, because the recieving `PollableTask` was dropped"), |
where future_name
is std::any::type_name::<[F]>()
, i.e. the type name of the future and return_name
was std::any::type_name::<T>()
Additionally, it might be worth tracking the caller location of this function for this diagnostic. That would need to be outside of the async move
block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How to do the tracking of the caller location?
return; | ||
} | ||
|
||
std::thread::sleep(Duration::from_secs_f32(1. / 100.)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sleeping in tests is very much not ideal. I think we would check the output from the relevant channel from each task
I.e. write this test relatviely async
, using e.g. TaskPoll::scope
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite get how I should use TaskPool::scope
here. I want to test that a PollableTask
will return something. Should I wrap those pollable tasks in a scope to do the waiting there?
Err(try_error) => match try_error { | ||
TryRecvError::Empty => None, | ||
TryRecvError::Closed => { | ||
panic!("Polling on the task failed because the connection was already closed.") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this panic-worthy? I'm not sure exactly when the actual spawned future is dropped, but I think it would be reasonable for the executor to drop it once it reaches Ready
once.
I'd say it might be better to add e.g. an AtomicBool
to this object, and print a warning in this case, at most once for each task. I'm not sure of the exact details of the executor though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added the panic due to this discussion: #4102 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, it seems quite harsh to stop the world for running the check after the task has ended.
I don't want to block on this; I don't feel very strongly either way.
We need to decide between this approach and the one in #4627, correct? |
Given the perf results I think this PR would be preferred. I think we're just waiting on DJMcNab's comments to be addressed. |
After the rebase, the example I initially changed became irrelevant (https://github.com/bevyengine/bevy/blob/main/examples/async_tasks/async_compute.rs). Should I add another one or is the unit test enough? |
@pubrrr I'd like to see another example; working with async tasks like this is a common point of confusion. |
Objective
add TaskPool::spawn_pollable so that the consumer does not need a block_on method
Solution
add TaskPool::spawn_pollable that uses a receiver to get the result of the async task.
This is basically a rework of PR #2691 with the review comments incorporated. In PR #4062 this was considered the better option.
Also see #2677 and #4045.