-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Scoped threads in the standard library, take 2 #3151
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
Changes from 8 commits
c67fffc
e4808b2
a76bcf5
4158f90
8db9e27
d53412a
c38bca6
ab04ad6
97915b8
6de9eb3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| - Feature Name: scoped_threads | ||
| - Start Date: 2019-02-26 | ||
| - RFC PR: (leave this empty) | ||
| - Rust Issue: (leave this empty) | ||
|
|
||
| # Summary | ||
| [summary]: #summary | ||
|
|
||
| Add scoped threads to the standard library that allow one to spawn threads | ||
| borrowing variables from the parent thread. | ||
|
|
||
| Example: | ||
|
|
||
| ```rust | ||
| let var = String::from("foo"); | ||
|
|
||
| thread::scope(|s| { | ||
| s.spawn(|_| println!("borrowed from thread #1: {}", var)); | ||
| s.spawn(|_| println!("borrowed from thread #2: {}", var)); | ||
| }); | ||
| ``` | ||
|
|
||
| # Motivation | ||
| [motivation]: #motivation | ||
|
|
||
| Before Rust 1.0 was released, we had | ||
| [`thread::scoped()`](https://docs.rs/thread-scoped/1.0.2/thread_scoped/) with the same | ||
| purpose as scoped threads, but then discovered it has a soundness issue that | ||
| could lead to use-after-frees so it got removed. This historical event is known as | ||
| [leakpocalypse](http://cglab.ca/~abeinges/blah/everyone-poops/). | ||
|
|
||
| Fortunately, the old scoped threads could be fixed by relying on closures rather than | ||
| guards to ensure spawned threads get automatically joined. But we weren't | ||
| feeling completely comfortable with including scoped threads in Rust 1.0 so it | ||
| was decided they should live in external crates, with the possibility of going | ||
| back into the standard library sometime in the future. | ||
| Four years have passed since then and the future is now. | ||
|
|
||
| Scoped threads in [Crossbeam](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html) | ||
| have matured through years of experience and today we have a design that feels solid | ||
| enough to be promoted into the standard library. | ||
|
|
||
| See the [Rationale and alternatives](#rationale-and-alternatives) section for more. | ||
|
|
||
| # Guide-level explanation | ||
| [guide-level-explanation]: #guide-level-explanation | ||
|
|
||
| The "hello world" of thread spawning might look like this: | ||
|
|
||
| ```rust | ||
| let greeting = String::from("Hello world!"); | ||
|
|
||
| let handle = thread::spawn(move || { | ||
| println!("thread #1 says: {}", greeting); | ||
| }); | ||
|
|
||
| handle.join().unwrap(); | ||
| ``` | ||
|
|
||
| Now let's try spawning two threads that use the same greeting. | ||
| Unfortunately, we'll have to clone it because | ||
| [`thread::spawn()`](https://doc.rust-lang.org/std/thread/fn.spawn.html) | ||
| has the `F: 'static` requirement, meaning threads cannot borrow local variables: | ||
|
|
||
| ```rust | ||
| let greeting = String::from("Hello world!"); | ||
|
|
||
| let handle1 = thread::spawn({ | ||
| let greeting = greeting.clone(); | ||
| move || { | ||
| println!("thread #1 says: {}", greeting); | ||
| } | ||
| }); | ||
|
|
||
| let handle2 = thread::spawn(move || { | ||
| println!("thread #2 says: {}", greeting); | ||
| }); | ||
|
|
||
| handle1.join().unwrap(); | ||
| handle2.join().unwrap(); | ||
| ``` | ||
|
|
||
| Scoped threads to the rescue! By opening a new `thread::scope()` block, | ||
| we can prove to the compiler that all threads spawned within this scope will | ||
| also die inside the scope: | ||
|
|
||
| ```rust | ||
| let greeting = String::from("Hello world!"); | ||
|
|
||
| thread::scope(|s| { | ||
| let handle1 = s.spawn(|_| { | ||
| println!("thread #1 says: {}", greeting); | ||
| }); | ||
|
|
||
| let handle2 = s.spawn(|_| { | ||
| println!("thread #2 says: {}", greeting); | ||
| }); | ||
|
|
||
| handle1.join().unwrap(); | ||
| handle2.join().unwrap(); | ||
| }); | ||
| ``` | ||
|
|
||
| That means variables living outside the scope can be borrowed without any | ||
| problems! | ||
|
|
||
| Now we don't have to join threads manually anymore because all unjoined threads | ||
| will be automatically joined at the end of the scope: | ||
|
|
||
| ```rust | ||
| let greeting = String::from("Hello world!"); | ||
|
|
||
| thread::scope(|s| { | ||
| s.spawn(|_| { | ||
| println!("thread #1 says: {}", greeting); | ||
| }); | ||
|
|
||
| s.spawn(|_| { | ||
| println!("thread #2 says: {}", greeting); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| When taking advantage of automatic joining in this way, note that `thread::scope()` | ||
| will panic if any of the automatically joined threads has panicked. | ||
|
|
||
| You might've noticed that scoped threads now take a single argument, which is | ||
| just another reference to `s`. Since `s` lives inside the scope, we cannot borrow | ||
| it directly. Use the passed argument instead to spawn nested threads: | ||
|
|
||
| ```rust | ||
| thread::scope(|s| { | ||
| s.spawn(|s| { | ||
| s.spawn(|_| { | ||
| println!("I belong to the same `thread::scope()` as my parent thread") | ||
| }); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| # Reference-level explanation | ||
| [reference-level-explanation]: #reference-level-explanation | ||
|
|
||
| We add two new types to the `std::thread` module: | ||
|
|
||
| ```rust | ||
| struct Scope<'env> {} | ||
| struct ScopedJoinHandle<'scope, T> {} | ||
| ``` | ||
|
|
||
| Lifetime `'env` represents the environment outside the scope, while | ||
| `'scope` represents the scope itself. More precisely, everything | ||
| outside the scope outlives `'env` and `'scope` outlives everything | ||
| inside the scope. The lifetime relations are: | ||
|
|
||
| ``` | ||
| 'variables_outside: 'env: 'scope: 'variables_inside | ||
| ``` | ||
|
|
||
| Next, we need the `scope()` and `spawn()` functions: | ||
|
|
||
| ```rust | ||
| fn scope<'env, F, T>(f: F) -> T | ||
| where | ||
| F: FnOnce(&Scope<'env>) -> T; | ||
|
|
||
| impl<'env> Scope<'env> { | ||
| fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> | ||
| where | ||
| F: FnOnce(&Scope<'env>) -> T + Send + 'env, | ||
| T: Send + 'env; | ||
| } | ||
| ``` | ||
|
|
||
| That's the gist of scoped threads, really. | ||
|
|
||
| Now we just need two more things to make the API complete. First, `ScopedJoinHandle` | ||
| is equivalent to `JoinHandle` but tied to the `'scope` lifetime, so it will have | ||
| the same methods. Second, the thread builder needs to be able to spawn threads | ||
| inside a scope: | ||
|
|
||
| ```rust | ||
| impl<'scope, T> ScopedJoinHandle<'scope, T> { | ||
| fn join(self) -> Result<T>; | ||
| fn thread(&self) -> &Thread; | ||
| } | ||
|
|
||
| impl Builder { | ||
| fn spawn_scoped<'scope, 'env, F, T>( | ||
| self, | ||
| &'scope Scope<'env>, | ||
bstrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| f: F, | ||
| ) -> io::Result<ScopedJoinHandle<'scope, T>> | ||
| where | ||
| F: FnOnce(&Scope<'env>) -> T + Send + 'env, | ||
| T: Send + 'env; | ||
| } | ||
| ``` | ||
|
|
||
| # Drawbacks | ||
| [drawbacks]: #drawbacks | ||
|
|
||
| The main drawback is that scoped threads make the standard library a little bit bigger. | ||
|
|
||
| # Rationale and alternatives | ||
| [rationale-and-alternatives]: #rationale-and-alternatives | ||
|
|
||
| The alternative is to keep scoped threads in external crates. However, there are | ||
| several advantages to having them in the standard library: | ||
|
|
||
| * This is a very common and useful utility and is great for learning, testing, and exploratory | ||
| programming. Every person learning Rust will at some point encounter interaction | ||
| of borrowing and threads. There's a very important lesson to be taught that threads | ||
| *can* in fact borrow local variables, but the standard library doesn't reflect this. | ||
|
|
||
| * Some might argue we should discourage using threads altogether and point people to | ||
| executors like Rayon and Tokio instead. But still, | ||
| the fact that `thread::spawn()` requires `F: 'static` and there's no way around it | ||
| feels like a missing piece in the standard library. | ||
|
|
||
| * Implementing scoped threads is very tricky to get right so it's good to have a | ||
| reliable solution provided by the standard library. | ||
|
|
||
| * There are many examples in the official documentation and books that could be | ||
| simplified by scoped threads. | ||
|
|
||
| * Scoped threads are typically a better default than `thread::spawn()` because | ||
| they make sure spawned threads are joined and don't get accidentally "leaked". | ||
| This is sometimes a problem in unit tests, where "dangling" threads can accumulate | ||
| if unit tests spawn threads and forget to join them. | ||
|
|
||
| * Users keep asking for scoped threads on IRC and forums | ||
| all the time. Having them as a "blessed" pattern in `std::thread` would be beneficial | ||
| to everyone. | ||
|
|
||
| # Prior art | ||
| [prior-art]: #prior-art | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prior art also exists in Swift's |
||
|
|
||
| Crossbeam has had | ||
| [scoped threads](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html) | ||
| since Rust 1.0. | ||
|
|
||
| There are two designs Crossbeam's scoped threads went through. The old one is from | ||
| the time `thread::scoped()` got removed and we wanted a sound alternative for the | ||
| Rust 1.0 era. The new one is from the last year's big revamp: | ||
|
|
||
| * Old: https://docs.rs/crossbeam/0.2.12/crossbeam/fn.scope.html | ||
| * New: https://docs.rs/crossbeam/0.7.1/crossbeam/fn.scope.html | ||
|
|
||
| There are several differences between old and new scoped threads: | ||
|
|
||
| 1. `scope()` now propagates unhandled panics from child threads. | ||
| In the old design, panics were silently ignored. | ||
| Users can still handle panics by manually working with `ScopedJoinHandle`s. | ||
|
|
||
| 2. The closure passed to `Scope::spawn()` now takes a `&Scope<'env>` argument that | ||
| allows one to spawn nested threads, which was not possible with the old design. | ||
| Rayon similarly passes a reference to child tasks. | ||
|
|
||
| 3. We removed `Scope::defer()` because it is not really useful, had bugs, and had | ||
| non-obvious behavior. | ||
|
|
||
| 4. `ScopedJoinHandle` got parametrized over `'scope` in order to prevent it from | ||
| escaping the scope. | ||
|
|
||
| Rayon also has [scopes](https://docs.rs/rayon/1.0.3/rayon/struct.Scope.html), | ||
| 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 NamingInstead I would prefer we follow Swift's example, using simpler naming, and go with - 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 // 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,
}
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| [unresolved-questions]: #unresolved-questions | ||
|
|
||
| Can this concept be extended to async? Would there be any behavioral or API differences? | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
|
|
||
| # Future possibilities | ||
| [future-possibilities]: #future-possibilities | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should probably reference "async scopes" #2647 (comment). |
||
|
|
||
| In the future, we could also have a threadpool like Rayon that can spawn | ||
| scoped tasks. | ||
Uh oh!
There was an error while loading. Please reload this page.