-
-
Notifications
You must be signed in to change notification settings - Fork 2.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
task: stabilize JoinSet
#4535
Comments
I'm prototyping a |
|
One major deficiency of the current I noticed this while attempting to rewrite This means that if you want to use Personally I think we should just expose the |
I am in favor of exposing the poll methods, though I will note that you can definitely use it with |
Yeah, I considered an approach like that, where the |
Merged an implementation of a We should probably do a release of both |
@hawkw I'd agree with this |
I would like to see a method on It seems like the easiest way might be to just make |
I also want to note that the signature of For example, say you have a task that manages a queue of downloads; the first solution you might think of could look something like this: async fn download_manager(
client: reqwest::Client,
notify: Arc<Notify>,
results: broadcast::Sender<DownloadResult>,
queue_size: usize,
) {
let mut pending = VecDeque::new();
let mut transfers = JoinSet::new();
fill_pending_queue(&mut pending).await;
loop {
while transfers.len() < queue_size {
if let Some(request) = pending.pop_front() {
let client = client.clone();
transfers.spawn(async move { do_download(&client, request).await });
} else {
break;
}
}
// We want to wait either for a new request to come in, or for a queued download to complete.
tokio::select! {
res = transfers.join_one() => match res {
Ok(Some(result)) => { let _ = results.send(result).await; }
Ok(None) => (),
Err(join_err) => { let _ = results.send(DownloadResult::from_join_err(join_err).await; },
},
_ = notify.notified() => {
fill_pending_queue(&mut pending).await;
}
};
}
} Except, surprise! When The fix is to map with tokio::select! {
Some(res) = transfers.join_one().map(Result::transpose) => match res { ... },
...
} Which is pretty simple, but isn't necessarily obvious, and I could see someone spending a long time trying to figure out why their process runs at 100% on one core periodically. I see the discussion here: #4335 (review) but I feel like a potential performance footgun probably trumps a minor ergonomic improvement. |
The |
Yeah, I agree with @Darksonn on this one...making the
IMO it seems like the best approach for this is to add spawning functions to Edit: Thinking about that a bit more, maybe the best thing to do is add a builder method to "attach" the builder to a let mut joinset = JoinSet::new();
tokio::task::Builder::new()
.with_join_set(joinset)
.name("whatever")
.spawn(some_task()); instead of let mut joinset = JoinSet::new();
let builder = tokio::task::Builder::new()
.name("whatever");
joinset.spawn_builder(builder, task); then we could just have the builder continue to provide If we added
which feels kind of overwhelming... The issue is that adding a |
I think for one, I think adding typestate to pub mod builder {
// Just putting these in a submodule to avoid polluting the `task` module namespace
// For both of these types, if `None` then fetch the current applicable context on call to `.spawn[_into]()`
pub struct WithRuntime<'a>(Option<&'a Handle>);
pub struct WithLocalSet<'a>(Option<&'a LocalSet>);
}
pub struct Builder<'a, Target = WithRuntime<'static>> {
// ...
}
impl Builder<'_> {
pub fn new() -> Self { ... }
}
impl Builder<'_, WithLocalSet<'static>> {
pub fn new_local() -> Self { ... }
}
impl<'a, Target> Builder<'a, Target> {
pub fn name(&self, name: &'a str) -> Self { ... }
pub fn with_runtime<'rt>(&self, runtime: &'r Handle) -> Builder<'a, WithRuntime<'rt>> { ... }
pub fn with_local_set<'ls>(&self, local_set: &'ls LocalSet> -> Builder<'a, WithLocalSet<'ls>> { ... }
}
impl<'a, 'rt> Builder<'a, WithRuntime<'rt>> {
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
where F: Future + Send + 'static, F::Output: Send + 'static
{ ... }
pub fn spawn_into<F>(&self, join_set: &mut JoinSet<F::Output>, task: F) -> AbortHandle
where F: Future + Send + 'static, F::Output: Send + 'static
{ ... }
}
impl<'a, 'ls> Builder<'a, WithLocalSet<'ls>> {
pub fn spawn<F>(&self, task: F) -> JoinHandle<F::Output>
where F: Future + 'static, F::Output: 'static
{ ... }
pub fn spawn_into<F>(&self, join_set: &mut JoinSet<F::Output>, task: F) -> AbortHandle
where F: Future + 'static, F::Output: 'static
{ ... }
} I'm not sure how this could integrate with |
Here's my proposal for making What do others think? |
Is there any interest in having |
We don't want 0.x crates in our public API, so we can't use |
This looks like it would require exposing |
We can expose |
Should I include that (as a separate commit) in my PR, or would you prefer me making a separate PR for that? |
I guess making a separate PR would be good. |
Closes #4535. This leaves the ID-related APIs unstable.
Closes #4535. This leaves the ID-related APIs unstable.
Closes #4535. This leaves the ID-related APIs unstable.
This is a meta issue tracking the stabilization of
JoinSet
. Outstanding issues:JoinSet
to not return cancelledJoinError
s #4534.JoinMap
implemented? A layer on top ofJoinSet
or a completely separate type?The text was updated successfully, but these errors were encountered: