Skip to content

Commit 4f235d3

Browse files
authored
Merge pull request #3151 from bstrie/scoped-threads-draft-2021-06-09
Scoped threads in the standard library, take 2
2 parents 3382121 + 6de9eb3 commit 4f235d3

File tree

1 file changed

+294
-0
lines changed

1 file changed

+294
-0
lines changed

text/3151-scoped-threads.md

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
- Feature Name: scoped_threads
2+
- Start Date: 2019-02-26
3+
- RFC PR: [rust-lang/rfcs#3151](https://github.com/rust-lang/rfcs/pull/3151)
4+
- Rust Issue: [rust-lang/rust#93203](https://github.com/rust-lang/rust/issues/93203)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Add scoped threads to the standard library that allow one to spawn threads
10+
borrowing variables from the parent thread.
11+
12+
Example:
13+
14+
```rust
15+
let var = String::from("foo");
16+
17+
thread::scope(|s| {
18+
s.spawn(|_| println!("borrowed from thread #1: {}", var));
19+
s.spawn(|_| println!("borrowed from thread #2: {}", var));
20+
});
21+
```
22+
23+
# Motivation
24+
[motivation]: #motivation
25+
26+
Before Rust 1.0 was released, we had
27+
[`thread::scoped()`](https://docs.rs/thread-scoped/1.0.2/thread_scoped/) with the same
28+
purpose as scoped threads, but then discovered it has a soundness issue that
29+
could lead to use-after-frees so it got removed. This historical event is known as
30+
[leakpocalypse](http://cglab.ca/~abeinges/blah/everyone-poops/).
31+
32+
Fortunately, the old scoped threads could be fixed by relying on closures rather than
33+
guards to ensure spawned threads get automatically joined. But we weren't
34+
feeling completely comfortable with including scoped threads in Rust 1.0 so it
35+
was decided they should live in external crates, with the possibility of going
36+
back into the standard library sometime in the future.
37+
Four years have passed since then and the future is now.
38+
39+
Scoped threads in [Crossbeam](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html)
40+
have matured through years of experience and today we have a design that feels solid
41+
enough to be promoted into the standard library.
42+
43+
See the [Rationale and alternatives](#rationale-and-alternatives) section for more.
44+
45+
# Guide-level explanation
46+
[guide-level-explanation]: #guide-level-explanation
47+
48+
The "hello world" of thread spawning might look like this:
49+
50+
```rust
51+
let greeting = String::from("Hello world!");
52+
53+
let handle = thread::spawn(move || {
54+
println!("thread #1 says: {}", greeting);
55+
});
56+
57+
handle.join().unwrap();
58+
```
59+
60+
Now let's try spawning two threads that use the same greeting.
61+
Unfortunately, we'll have to clone it because
62+
[`thread::spawn()`](https://doc.rust-lang.org/std/thread/fn.spawn.html)
63+
has the `F: 'static` requirement, meaning threads cannot borrow local variables:
64+
65+
```rust
66+
let greeting = String::from("Hello world!");
67+
68+
let handle1 = thread::spawn({
69+
let greeting = greeting.clone();
70+
move || {
71+
println!("thread #1 says: {}", greeting);
72+
}
73+
});
74+
75+
let handle2 = thread::spawn(move || {
76+
println!("thread #2 says: {}", greeting);
77+
});
78+
79+
handle1.join().unwrap();
80+
handle2.join().unwrap();
81+
```
82+
83+
Scoped threads to the rescue! By opening a new `thread::scope()` block,
84+
we can prove to the compiler that all threads spawned within this scope will
85+
also die inside the scope:
86+
87+
```rust
88+
let greeting = String::from("Hello world!");
89+
90+
thread::scope(|s| {
91+
let handle1 = s.spawn(|_| {
92+
println!("thread #1 says: {}", greeting);
93+
});
94+
95+
let handle2 = s.spawn(|_| {
96+
println!("thread #2 says: {}", greeting);
97+
});
98+
99+
handle1.join().unwrap();
100+
handle2.join().unwrap();
101+
});
102+
```
103+
104+
That means variables living outside the scope can be borrowed without any
105+
problems!
106+
107+
Now we don't have to join threads manually anymore because all unjoined threads
108+
will be automatically joined at the end of the scope:
109+
110+
```rust
111+
let greeting = String::from("Hello world!");
112+
113+
thread::scope(|s| {
114+
s.spawn(|_| {
115+
println!("thread #1 says: {}", greeting);
116+
});
117+
118+
s.spawn(|_| {
119+
println!("thread #2 says: {}", greeting);
120+
});
121+
});
122+
```
123+
124+
When taking advantage of automatic joining in this way, note that `thread::scope()`
125+
will panic if any of the automatically joined threads has panicked.
126+
127+
You might've noticed that scoped threads now take a single argument, which is
128+
just another reference to `s`. Since `s` lives inside the scope, we cannot borrow
129+
it directly. Use the passed argument instead to spawn nested threads:
130+
131+
```rust
132+
thread::scope(|s| {
133+
s.spawn(|s| {
134+
s.spawn(|_| {
135+
println!("I belong to the same `thread::scope()` as my parent thread")
136+
});
137+
});
138+
});
139+
```
140+
141+
# Reference-level explanation
142+
[reference-level-explanation]: #reference-level-explanation
143+
144+
We add two new types to the `std::thread` module:
145+
146+
```rust
147+
struct Scope<'env> {}
148+
struct ScopedJoinHandle<'scope, T> {}
149+
```
150+
151+
Lifetime `'env` represents the environment outside the scope, while
152+
`'scope` represents the scope itself. More precisely, everything
153+
outside the scope outlives `'env` and `'scope` outlives everything
154+
inside the scope. The lifetime relations are:
155+
156+
```
157+
'variables_outside: 'env: 'scope: 'variables_inside
158+
```
159+
160+
Next, we need the `scope()` and `spawn()` functions:
161+
162+
```rust
163+
fn scope<'env, F, T>(f: F) -> T
164+
where
165+
F: FnOnce(&Scope<'env>) -> T;
166+
167+
impl<'env> Scope<'env> {
168+
fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
169+
where
170+
F: FnOnce(&Scope<'env>) -> T + Send + 'env,
171+
T: Send + 'env;
172+
}
173+
```
174+
175+
That's the gist of scoped threads, really.
176+
177+
Now we just need two more things to make the API complete. First, `ScopedJoinHandle`
178+
is equivalent to `JoinHandle` but tied to the `'scope` lifetime, so it will have
179+
the same methods. Second, the thread builder needs to be able to spawn threads
180+
inside a scope:
181+
182+
```rust
183+
impl<'scope, T> ScopedJoinHandle<'scope, T> {
184+
fn join(self) -> Result<T>;
185+
fn thread(&self) -> &Thread;
186+
}
187+
188+
impl Builder {
189+
fn spawn_scoped<'scope, 'env, F, T>(
190+
self,
191+
&'scope Scope<'env>,
192+
f: F,
193+
) -> io::Result<ScopedJoinHandle<'scope, T>>
194+
where
195+
F: FnOnce(&Scope<'env>) -> T + Send + 'env,
196+
T: Send + 'env;
197+
}
198+
```
199+
200+
# Drawbacks
201+
[drawbacks]: #drawbacks
202+
203+
The main drawback is that scoped threads make the standard library a little bit bigger.
204+
205+
# Rationale and alternatives
206+
[rationale-and-alternatives]: #rationale-and-alternatives
207+
208+
* Keep scoped threads in external crates.
209+
210+
There are several advantages to having them in the standard library:
211+
212+
* This is a very common and useful utility and is great for learning, testing, and exploratory
213+
programming. Every person learning Rust will at some point encounter interaction
214+
of borrowing and threads. There's a very important lesson to be taught that threads
215+
*can* in fact borrow local variables, but the standard library doesn't reflect this.
216+
217+
* Some might argue we should discourage using threads altogether and point people to
218+
executors like Rayon and Tokio instead. But still,
219+
the fact that `thread::spawn()` requires `F: 'static` and there's no way around it
220+
feels like a missing piece in the standard library.
221+
222+
* Implementing scoped threads is very tricky to get right so it's good to have a
223+
reliable solution provided by the standard library.
224+
225+
* There are many examples in the official documentation and books that could be
226+
simplified by scoped threads.
227+
228+
* Scoped threads are typically a better default than `thread::spawn()` because
229+
they make sure spawned threads are joined and don't get accidentally "leaked".
230+
This is sometimes a problem in unit tests, where "dangling" threads can accumulate
231+
if unit tests spawn threads and forget to join them.
232+
233+
* Users keep asking for scoped threads on IRC and forums
234+
all the time. Having them as a "blessed" pattern in `std::thread` would be beneficial
235+
to everyone.
236+
237+
* Return a `Result` from `scope` with all the captured panics.
238+
239+
* This quickly gets complicated, as multiple threads might have panicked.
240+
Returning a `Vec` or other collection of panics isn't always the most useful interface,
241+
and often unnecessary. Explicitly using `.join()` on the `ScopedJoinHandle`s to
242+
handle panics is the most flexible and efficient way to handle panics, if the user wants
243+
to handle them.
244+
245+
* Don't pass a `&Scope` argument to the threads.
246+
247+
* `scope.spawn(|| ..)` rather than `scope.spawn(|scope| ..)` would require the `move` keyword
248+
(`scope.spawn(move || ..)`) if you want to use the scope inside that closure, which gets unergonomic.
249+
250+
251+
# Prior art
252+
[prior-art]: #prior-art
253+
254+
Crossbeam has had
255+
[scoped threads](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html)
256+
since Rust 1.0.
257+
258+
There are two designs Crossbeam's scoped threads went through. The old one is from
259+
the time `thread::scoped()` got removed and we wanted a sound alternative for the
260+
Rust 1.0 era. The new one is from the last year's big revamp:
261+
262+
* Old: https://docs.rs/crossbeam/0.2.12/crossbeam/fn.scope.html
263+
* New: https://docs.rs/crossbeam/0.7.1/crossbeam/fn.scope.html
264+
265+
There are several differences between old and new scoped threads:
266+
267+
1. `scope()` now propagates unhandled panics from child threads.
268+
In the old design, panics were silently ignored.
269+
Users can still handle panics by manually working with `ScopedJoinHandle`s.
270+
271+
2. The closure passed to `Scope::spawn()` now takes a `&Scope<'env>` argument that
272+
allows one to spawn nested threads, which was not possible with the old design.
273+
Rayon similarly passes a reference to child tasks.
274+
275+
3. We removed `Scope::defer()` because it is not really useful, had bugs, and had
276+
non-obvious behavior.
277+
278+
4. `ScopedJoinHandle` got parametrized over `'scope` in order to prevent it from
279+
escaping the scope.
280+
281+
Rayon also has [scopes](https://docs.rs/rayon/1.0.3/rayon/struct.Scope.html),
282+
but they work on a different abstraction level - Rayon spawns tasks rather than
283+
threads. Its API is the same as the one proposed in this RFC.
284+
285+
# Unresolved questions
286+
[unresolved-questions]: #unresolved-questions
287+
288+
Can this concept be extended to async? Would there be any behavioral or API differences?
289+
290+
# Future possibilities
291+
[future-possibilities]: #future-possibilities
292+
293+
In the future, we could also have a threadpool like Rayon that can spawn
294+
scoped tasks.

0 commit comments

Comments
 (0)