|
| 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