Skip to content

Commit c67fffc

Browse files
author
Stjepan Glavina
committed
Add scoped threads to the standard library
1 parent ffecb55 commit c67fffc

File tree

1 file changed

+268
-0
lines changed

1 file changed

+268
-0
lines changed

text/0000-scoped-threads.md

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
- Feature Name: scoped_threads
2+
- Start Date: 2019-02-26
3+
- RFC PR: (leave this empty)
4+
- Rust Issue: (leave this empty)
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+
.unwrap();
22+
```
23+
24+
# Motivation
25+
[motivation]: #motivation
26+
27+
Before Rust 1.0 was released, we had
28+
[`thread::scoped()`](https://docs.rs/thread-scoped/1.0.2/thread_scoped/) with the same
29+
purpose as scoped threads, but then discovered it has a soundness issue that
30+
could lead to use-after-frees so it got removed. This historical event is known as
31+
[leakpocalypse](http://cglab.ca/~abeinges/blah/everyone-poops/).
32+
33+
Fortunately, the old scoped threads could be fixed by relying on closures rather than
34+
guards to ensure spawned threads get automatically joined. But we weren't
35+
feeling completely comfortable with including scoped threads in Rust 1.0 so it
36+
was decided they should live in external crates, with the possibility of going
37+
back into the standard library sometime in the future.
38+
Four years have passed since then and the future is now.
39+
40+
Scoped threads in [Crossbeam](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html)
41+
have matured through years of experience and today we have a design that feels solid
42+
enough to be promoted into the standard library.
43+
44+
# Guide-level explanation
45+
[guide-level-explanation]: #guide-level-explanation
46+
47+
The "hello world" of thread spawning might look like this:
48+
49+
```rust
50+
let greeting = String::from("Hello world!");
51+
52+
let handle = thread::spawn(move || {
53+
println!("thread #1 says: {}", greeting);
54+
});
55+
56+
handle.join().unwrap();
57+
```
58+
59+
Now let's try spawning two threads that use the same greeting.
60+
Unfortunately, we'll have to clone it because
61+
[`thread::spawn()`](https://doc.rust-lang.org/std/thread/fn.spawn.html)
62+
has the `F: 'static` requirement, meaning threads cannot borrow local variables:
63+
64+
```rust
65+
let greeting = String::from("Hello world!");
66+
67+
let handle1 = thread::spawn({
68+
let greeting = greeting.clone();
69+
move || {
70+
println!("thread #1 says: {}", greeting);
71+
}
72+
});
73+
74+
let handle2 = thread::spawn(move || {
75+
println!("thread #2 says: {}", greeting);
76+
});
77+
78+
handle1.join().unwrap();
79+
handle2.join().unwrap();
80+
```
81+
82+
Scoped threads coming to the rescue! By opening a new `thread::scope()` block,
83+
we can prove to the compiler that all threads spawned within this scope will
84+
also die inside the scope:
85+
86+
```rust
87+
let greeting = String::from("Hello world!");
88+
89+
thread::scope(|s| {
90+
let handle1 = s.spawn(|_| {
91+
println!("thread #1 says: {}", greeting);
92+
});
93+
94+
let handle2 = s.spawn(|_| {
95+
println!("thread #2 says: {}", greeting);
96+
});
97+
98+
handle1.join().unwrap();
99+
handle2.join().unwrap();
100+
})
101+
.unwrap();
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+
.unwrap();
123+
```
124+
125+
Note that `thread::scope()` returns a `Result` that will be `Ok` if all
126+
automatically joined threads have successfully completed, i.e. they haven't
127+
panicked.
128+
129+
You might've noticed that scoped threads now take a single argument, which is
130+
just another reference to `s`. Since `s` lives inside the scope, we cannot borrow
131+
it directly. Use the passed argument instead to spawn nested threads:
132+
133+
```rust
134+
thread::scope(|s| {
135+
s.spawn(|s| {
136+
s.spawn(|_| {
137+
println!("I belong to the same `thread::scope()` as my parent thread")
138+
});
139+
});
140+
})
141+
.unwrap();
142+
```
143+
144+
# Reference-level explanation
145+
[reference-level-explanation]: #reference-level-explanation
146+
147+
We add two new types to the `std::thread` module:
148+
149+
```rust
150+
struct Scope<'env> {}
151+
struct ScopedJoinHandle<'scope, T> {}
152+
```
153+
154+
Lifetime `'env` represents the environment outside the scope, while
155+
`'scope` represents the scope itself. More precisely, everything
156+
outside the scope outlives `'env` and `'scope` outlives everything
157+
inside the scope. The lifetime relations are:
158+
159+
```
160+
'variables_outside: 'env: 'scope: 'variables_inside
161+
```
162+
163+
Next, we need the `scope()` and `spawn()` functions:
164+
165+
```rust
166+
fn scope<'env, F, T>(f: F) -> Result<T>
167+
where
168+
F: FnOnce(&Scope<'env>) -> T;
169+
170+
impl<'env> Scope<'env> {
171+
fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T>
172+
where
173+
F: FnOnce(&Scope<'env>) -> T + Send + 'env,
174+
T: Send + 'env;
175+
}
176+
```
177+
178+
That's the gist of scoped threads, really.
179+
180+
Now we just need two more things to make the API complete. First, `ScopedJoinHandle`
181+
is equivalent to `JoinHandle` but tied to the `'scope` lifetime, so it will have
182+
the same methods. Second, the thread builder needs to be able to spawn threads
183+
inside a scope:
184+
185+
```rust
186+
impl<'scope, T> ScopedJoinHandle<'scope, T> {
187+
fn join(self) -> Result<T>;
188+
fn thread(&self) -> &Thread;
189+
}
190+
191+
impl Builder {
192+
fn spawn_scoped<'scope, 'env, F, T>(
193+
self,
194+
&'scope Scope<'env>,
195+
f: F,
196+
) -> io::Result<ScopedJoinHandle<'scope, T>>
197+
where
198+
F: FnOnce(&Scope<'env>) -> T + Send + 'env,
199+
T: Send + 'env;
200+
}
201+
```
202+
203+
It's also worth pointing out what exactly happens at the scope end when all
204+
unjoined threads get automatically joined. If all joins succeed, we take
205+
the result of the main closure passed to `scope()` and wrap it inside `Ok`.
206+
207+
If any thread panics (and in fact multiple threads can panic), we collect
208+
all those panics into a `Vec`, box it, and finally wrap it inside `Err`.
209+
The error type is then erased because `thread::Result<T>` is just an
210+
alias for:
211+
212+
```rust
213+
Result<T, Box<dyn Any + Send + 'static>>
214+
```
215+
216+
This way we can do `thread::scope(...).unwrap()` to propagate all panics
217+
in child threads into the main parent thread.
218+
219+
If the main `scope()` closure has panicked after spawning threads, we
220+
just resume unwinding after joining child threads.
221+
222+
# Drawbacks
223+
[drawbacks]: #drawbacks
224+
225+
The main drawback is that scoped threads make the standard library a little bit bigger.
226+
227+
# Rationale and alternatives
228+
[rationale-and-alternatives]: #rationale-and-alternatives
229+
230+
The alternative is to keep scoped threads in external crates. However, there are
231+
several advantages to having them in the standard library.
232+
233+
This is a very common and useful utility and is great for learning, testing, and exploratory
234+
programming. Every person learning Rust will at some point encounter interaction
235+
of borrowing and threads. There's a very important lesson to be taught that threads
236+
*can* in fact borrow local variables, but the standard library doesn't reflect this.
237+
238+
Some might argue we should discourage using threads altogether and point people to
239+
executors like Rayon and Tokio instead. But still,
240+
the fact that `thread::spawn()` requires `F: 'static` and there's no way around it
241+
feels like a missing piece in the standard library.
242+
243+
Finally, it's indisputable that users keep asking for scoped threads on IRC and forums
244+
all the time. Having them as a "blessed" pattern in `std::thread` would be beneficial
245+
to everyone.
246+
247+
# Prior art
248+
[prior-art]: #prior-art
249+
250+
Crossbeam has had
251+
[scoped threads](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html)
252+
since Rust 1.0.
253+
254+
Rayon also has [scopes](https://docs.rs/rayon/1.0.3/rayon/struct.Scope.html),
255+
but they work on a different abstraction level - Rayon spawns tasks rather than
256+
threads. Its API is almost the same as proposed in this RFC, the only
257+
difference being that `scope()` propagates panics instead of returning `Result`.
258+
This behavior makes more sense for tasks than threads.
259+
260+
# Unresolved questions
261+
[unresolved-questions]: #unresolved-questions
262+
263+
None.
264+
265+
# Future possibilities
266+
[future-possibilities]: #future-possibilities
267+
268+
None.

0 commit comments

Comments
 (0)