-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Scoped threads in the standard library, take 2 #3151
Scoped threads in the standard library, take 2 #3151
Conversation
Note that as discussed in #2647 (comment), it is possible to offer an API that doesn't take a parameter on the sub-closures: let var = &String::from("foo");
thread::scope(|s| {
s.spawn(move || {
println!("borrowed from thread #1: {}", var);
s.spawn(|| println!("Another one"));
});
s.spawn(|| println!("borrowed from thread #2: {}", var));
}); although it does require (a copy of) I personally believe that this tiny "detail" of having to add So whilst I believe that the current design is better, the |
text/0000-scoped-threads.md
Outdated
to everyone. | ||
|
||
# Prior art | ||
[prior-art]: #prior-art |
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.
Prior art also exists in Swift's TaskGroup
introduced in SE-0304. Instead of working directly on threads, it works with async "tasks" (and is closer to e.g. an async_std::task::Task
). But the design still covers a lot of the same space as we do in Rust, and as such I think it's worth to include in the prior art section.
text/0000-scoped-threads.md
Outdated
Can this concept be extended to async? Would there be any behavioral or API differences? | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities |
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 should probably reference "async scopes" #2647 (comment).
text/0000-scoped-threads.md
Outdated
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
Can this concept be extended to async? Would there be any behavioral or API differences? |
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'd like to read more on the reasoning why we're going for "scoped threads" rather than a "scoped thread pool" (see also: #2647 (comment)). I feel like these two concepts are closer to each other than one might intuitively assume, and it's important to cover the relationship between the two.
text/0000-scoped-threads.md
Outdated
but they work on a different abstraction level - Rayon spawns tasks rather than | ||
threads. Its API is the same as the one proposed in this RFC. | ||
|
||
# Unresolved questions |
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'd like to raise a point on naming here.
The ambiguity of "Scope"
I remember the first time I learned about "scoped threads" and being confused about what they do. I wondered whether it referred to:
- A "scope" as in "function scope"
- A "scope" as in "telemetry scope"
- Some other kind of "scope" specific to concurrent programming I was not yet aware of.
To this day I'm still not entirely clear on its origin. Though it seems likely that when this API was first introduced in 2014 (#461) the name is a reference to Boost C++ Scoped Threads, which roughly seems to use "scope" as an analog for "grouping of".
Alternative Naming
Instead I would prefer we follow Swift's example, using simpler naming, and go with ThreadGroup
:
- struct Scope<'env> {}
+ struct ThreadGroup<'env> {}
- fn scope<'env, F, T>(f: F) -> T {}
+ impl ThreadGroup<'env> {
+ fn new<'env, F, T>(f: F) -> T {}
+ }
- struct ScopedJoinHandle<'scope, T> {}
+ struct GroupJoinHandle<'scope, T> {}
The name ThreadGroup
implies a type containing threads which work as a group — which is exactly what this API does. Seeing ThreadGroup
referenced inside code should also be immediately clear as to what it does. If we contrast the two APIs:
// Immediately clear that this refers to a group of threads.
struct Thing {
group: ThreadGroup,
}
// `Scope` requires additional context to clarify what it does.
struct Thing {
scope: 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.
Scope means "lexical scope" (where "function scope" is one kind of lexical scope). They're named "scoped threads" because the API ensures the threads have exited before the scope ends.
A "thread group" is a much weaker concept - simply a set of threads that could be "joined" as one. A thread group need not be scoped.
Re-posting my comment from the last version of this RFC: I would like to register strong support here. I can't count the number of examples and documentation that would make use of scoped threads, and be way better for it. Furthermore, at least in the kind of code that I write, often, scoped threads are what I actually want, rather than thread::spawn. thread::spawn's signature is more general, but that means that it can be used in less situations, and I rarely need the power of thread::spawn. |
It's probably worth waiting to merge this until we've figured out how to support run-to-completion futures, given the possibility of a |
@Kestrer could you elaborate on why that would obsolete this API? This is a synchronous API which has existed in a crate for many years without needing to change once, and it's already very overdue in the standard library. A futures-based API might also be useful, but it would be unprecedented for an async API to live under |
@Kestrer Is there a proposal or prior discussion for run-to-completion futures that includes a |
The existence of a
Yes, it was proposed by @Dirbaio on zulip (archive). It does have problems (archive) but I still believe it to be an option. |
From that proposal:
I mean... this seems fundamentally impossible to begin with. Sync code doesn't yield so the only way this can work is if you prevent types with I think we may at some point get something like the
|
Notably, a |
What if this was implemented with a macro? The idea is that the macro would be responsible for safely creating the root scope, ensuring it doesn't get leaked by exploiting temporary lifetime extension. In practice this means that the macro will create the scope and immediately borrow it, thus creating an anonymous local. Since it's anonymous and the macro only exposes a shared reference the user can't leak it, because that requires ownership (maybe something could be done with mutable access, but that's avoided anyway). Thus the anonymous local will always be dropped by the compiler and the API should be sound. You can find a proof of concept in this playground (most code is copied from crossbeam, the main focus should be the The main pro of this approach would be avoiding the extra nesting layer for creating the scope, which in my opinion makes the ergonomics pretty bad. |
That is basically how |
Kinda. Both exploit macros to make some value not accessible to the user. While |
In sync code you can enforce cleanup runs just fine with closure-based APIs like the one proposed here. It's impossible for the execution flow to "magically vanish", the user closure either returns normally or it panics, in either case cleanup is guaranteed to run. In async code execution flow can indeed "magically vanish" at any
This doesn't work in async context: You can create the scope, suspend the future in an This is not a problem with |
I encourage proponents of |
It's worth noting that leaking itself is not the problem for the scope API, it's leaking non-'static data which is the problem, since this allows the lifetime to expire before the destructor is called. In an async context, as long as you only borrow from local variables, the returned future is still |
But you can have the scope borrow from outside the future: async fn boom(from_outside: &'a mut Foo) {
let scope = thread_scope!();
scope.spawn(|| do_stuff(from_outside); // this borrows data from outside the Future
whatever().await; // we suspend the future here and then leak it
// scope would be dropped here
} |
Possible extension: Generalize Examples of other code that could use the same
Disadvantages:
I think I just argued myself out of thinking this is a good idea, but perhaps someone has a better use case. |
I think forgetting a join handle sounds like intentional misuse of the API. As punishment for that crime, your screams of panic shall be silenced. |
One more interesting question: If a scoped thread panics, should // This invokes the panic handler twice. Once by panic!() on the spawned
// thread, and the second time by the panic in `unwrap()` on the main thread. The
// panic payload from the unwrap is the standard Result::unwrap message, not
// the payload from the first panic.
std::thread::spawn(|| panic!("!")).join().unwrap(); // But what about this? Does this also invoke the panic handler on both the
// spawned thread and the main thread? Or do we teleport the panic payload from
// the scoped thread to the main thread and continue unwinding the same panic
// there?
std::thread::scope(|s| s.spawn(|_| panic!("!"))); My current implementation uses However, it might take a significant amount of time before the panic is resumed, if other threads are still running. That might result in surprising timing of the panic message getting shown and the program terminating: std::thread::scope(|s| {
s.spawn(|_| panic!("!"));
s.spawn(|_| sleep(Duration::from_secs(10)));
}); This program will immediately show Thoughts? |
Thought about it some more, and I'm now convinced it should start a new panic:
Then each thread explains why that thread panicked. Otherwise the main thread panics without any It also simplifies the implementation. |
Implementation PR: rust-lang/rust#92555 It doesn't have any tests yet (other than doc tests). If anyone has the time and energy to help out with writing some useful tests, please do. :) (Comment on the PR first though, so we don't do double work.) |
@bstrie I've updated the alternatives section, which I think was the only to-do left on this RFC. Hope you don't mind. |
@rfcbot merge |
Team member @m-ou-se has proposed to merge this. The next step is review by the rest of the tagged team members: No concerns currently listed. Once a majority of reviewers approve (and at most 2 approvals are outstanding), this will enter its final comment period. If you spot a major issue that hasn't been raised at any point in this process, please speak up! See this document for info about what commands tagged team members can give me. |
|
This is exciting. I'm looking forward to seeing this back in std! |
🔔 This is now entering its final comment period, as per the review above. 🔔 |
The final comment period, with a disposition to merge, as per the review above, is now complete. As the automated representative of the governance process, I would like to thank the author for their work and everyone else who contributed. This will be merged soon. |
This RFC has been accepted and merged! 🎉 Tracking issue: rust-lang/rust#93203 Implementation: rust-lang/rust#92555 |
Implement RFC 3151: Scoped threads. This implements rust-lang/rfcs#3151 r? `@Amanieu`
Rendered url is broken |
The successor to #2647 .
Add scoped threads to the standard library that allow spawning threads that borrow variables from the parent thread.
Rendered