-
-
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
WIP: task::scope #2576
WIP: task::scope #2576
Conversation
This change adds task::scope as a mechanism for supporting structured concurrency as described in tokio-rs#1879. In this version of the change scopes can be created using a freestanding `task::scope` function, which creates a new detached scope (which is not coupled to the parent scope), `Scope::detached` and `Scope::with_parent`, and a `::child()` method on an existing scope handle. Since a few of of those methods are doing the same thing, some of those might get dropped before the change is merged. For future extensibility also a `Scope::with_config` and `ScopeConfigBuilder` are demonstrated in this change. Those might also be part of the initial change.
This replaces #2153 |
} | ||
|
||
match self.drop_behavior { | ||
ScopeDropBehavior::BlockToCompletion => { |
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.
We will probably want to leave out this version in the inital implementation since it raises a few ugly qeustions - eg around what to do in a singlethreaded runtime. I just left it here so far for demo purposes.
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.
Agreed, and even in multi-threaded, it could be unexpected to have the threads see some hangs too.
/// | ||
/// Tasks can wait for tracked tasks to finish by obtaining a Future via `wait`. | ||
/// This Future will get fulfilled when no tasks are running anymore. | ||
pub(crate) struct WaitGroup { |
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 haven't reviewed this file, but I think the concept of a WaitGroup
is probably worth being a separate PR that we can get in faster! Do you think this one is useful beyond the internal usage?
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.
It could definitely be exposed on its own and is helpful to have. It only didn't happen in this change since it would require other discussions on how the public API of WaitGroup
should look like (would it be what WaitGroup
or SharedWaitGroup
is here?)
} | ||
|
||
match self.drop_behavior { | ||
ScopeDropBehavior::BlockToCompletion => { |
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.
Agreed, and even in multi-threaded, it could be unexpected to have the threads see some hangs too.
Panic, | ||
/// When a scope is dropped while tasks are outstanding, the process will be | ||
/// aborted. | ||
Abort, |
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 there value in having the option vs someone just setting panic=abort
in their Cargo.toml
?
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.
Not too much. Maybe if someone mainly cares about not misusing scope
s. It's also a relict from the idea if to be able to borrow from the parent task inside child tasks. In order to guarantee that property a panic!
wouldn't be strong enough.
That said those APIs are probably all not needed for a v1, and a reasonable default could be picked. The options are mostly presented tho show how a scope could be configured in case configurability is desired.
F: FnOnce(ScopeHandle) -> Fut, | ||
Fut: Future<Output = R> + Send, | ||
{ | ||
Scope::detached().enter(scope_func).await |
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.
In your other PR about "implicit scopes", this would attach to the parent, but in this PR, this is supposed to be a global/detached scope, yes?
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.
The "implicit scopes" change would also get a method where you can spawn a task which is not directly attached to the parent to opt-out of structured concurrency if you want to. Currently both options would then spawn a "global" task, but I think in the end both variants should then spawn the task into the "runtime scope", which is the top level scope. Thereby shutting down the runtime would automatically cancel the remaining tasks and wait for them to complete. This however requires a change to the runtime structs to include this top level scope, which is not included yet.
|
||
let mut drop_behavior = match &self.parent { | ||
Some(parent) => parent.scope.config.drop_behavior, | ||
None => ScopeDropBehavior::Panic, |
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.
Should it really panic by default if you drop a scope without polling it to the end? I can imagine plenty of Rust patterns where you might start a task/scope at the beginning, and then try some other fallible method, and only afterwards scope.await
. If you were using ?
, it'd be pretty easy to drop the scope, and probably most would expect that the scope would just be canceled.
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.
As written in a few other places: It's for obvious reasons not a great strategy. But there exists no perfect strategy for this problem, and therefore this seems to be the best compromise.
- letting the background task continue to run (as proposed in RFC: structured concurrency #2596 ) will break structured concurrency and will not allow users to reason anymore about when subtasks (and their usage for certain resources) end
- doing a blocking wait will not work in single-threaded environments, and might lead to high latency variances in multithreaded environments.
In general, I think structured concurrency will be a huge help. I have actual experiences in the Linkerd proxy where we would accidentally leak tasks, and we really should be making it harder to do that. I want us to explore this space more! At the same time, I think there's some questions around default behavior that we need answers to, and I'm not sure we can get them without trying scopes out in an actual application. I think the questions about implicit vs explicit (my guess is a mix), how to trigger cancels (explicit calls vs drop), whether scopes dropping should panic by default, and graceful vs forceful shutdown really need Rust experience. This is because while we have a lot of research in other languages, they also don't perfectly match Rust. We have We also are trying to polish up a Tokio 1.0 this year. We only have a few months left. And we can't be changing the default behaviors once they've shipped. This doesn't dampen my desire to explore this, it just colors how I think we actually can. What if we take this and publish |
I'm going to close this now as there is no way forward. More details here #2596 (comment) Thanks again for all the work you did on this. Even though we didn't manage to get this through, I personally learned a lot on the topic and from your research. Also, once again, I'm sorry that didn't put more time into this sooner. |
This change adds task::scope as a mechanism for supporting
structured concurrency as described in #1879.
In this version of the change scopes can be created using a freestanding
task::scope
function, which creates a new detached scope (which is notcoupled to the parent scope),
Scope::detached
andScope::with_parent
,and a
::child()
method on an existing scope handle.Since a few of of those methods are doing the same thing, some of those
might get dropped before the change is merged. For future extensibility
also a
Scope::with_config
andScopeConfigBuilder
are demonstrated inthis change. Those might also be part of the initial change.