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