From c67fffc5555e7240baf0adab11e645aedfc31214 Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Tue, 26 Feb 2019 11:21:55 +0100 Subject: [PATCH 01/10] Add scoped threads to the standard library --- text/0000-scoped-threads.md | 268 ++++++++++++++++++++++++++++++++++++ 1 file changed, 268 insertions(+) create mode 100644 text/0000-scoped-threads.md diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md new file mode 100644 index 00000000..b61917a7 --- /dev/null +++ b/text/0000-scoped-threads.md @@ -0,0 +1,268 @@ +- 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)); +}) +.unwrap(); +``` + +# 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. + +# 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 coming 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(); +}) +.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); + }); +}) +.unwrap(); +``` + +Note that `thread::scope()` returns a `Result` that will be `Ok` if all +automatically joined threads have successfully completed, i.e. they haven't +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") + }); + }); +}) +.unwrap(); +``` + +# 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) -> Result +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; + fn thread(&self) -> &Thread; +} + +impl Builder { + fn spawn_scoped<'scope, 'env, F, T>( + self, + &'scope Scope<'env>, + f: F, + ) -> io::Result> + where + F: FnOnce(&Scope<'env>) -> T + Send + 'env, + T: Send + 'env; +} +``` + +It's also worth pointing out what exactly happens at the scope end when all +unjoined threads get automatically joined. If all joins succeed, we take +the result of the main closure passed to `scope()` and wrap it inside `Ok`. + +If any thread panics (and in fact multiple threads can panic), we collect +all those panics into a `Vec`, box it, and finally wrap it inside `Err`. +The error type is then erased because `thread::Result` is just an +alias for: + +```rust +Result> +``` + +This way we can do `thread::scope(...).unwrap()` to propagate all panics +in child threads into the main parent thread. + +If the main `scope()` closure has panicked after spawning threads, we +just resume unwinding after joining child threads. + +# 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. + +Finally, it's indisputable that 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 + +Crossbeam has had +[scoped threads](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/index.html) +since Rust 1.0. + +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 almost the same as proposed in this RFC, the only +difference being that `scope()` propagates panics instead of returning `Result`. +This behavior makes more sense for tasks than threads. + +# Unresolved questions +[unresolved-questions]: #unresolved-questions + +None. + +# Future possibilities +[future-possibilities]: #future-possibilities + +None. From e4808b277cd01032dc5c229d1cf44ace00f2b750 Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Tue, 26 Feb 2019 11:46:33 +0100 Subject: [PATCH 02/10] Add a link to Crossbeam's error handling --- text/0000-scoped-threads.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index b61917a7..6313db64 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -219,6 +219,9 @@ in child threads into the main parent thread. If the main `scope()` closure has panicked after spawning threads, we just resume unwinding after joining child threads. +Crossbeam's logic for error handling can be found +[here](https://github.com/crossbeam-rs/crossbeam/blob/79210d6ae34a3e84b23546d8abc5c4b81b206019/crossbeam-utils/src/thread.rs#L167-L193). + # Drawbacks [drawbacks]: #drawbacks From a76bcf579778988a56a603e3fa6d5e2e8f78be8a Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Wed, 27 Feb 2019 17:34:11 +0100 Subject: [PATCH 03/10] Expand rationale and remove ScopedJoinHandle --- text/0000-scoped-threads.md | 98 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index 6313db64..751c6720 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -41,6 +41,8 @@ Scoped threads in [Crossbeam](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/i 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 @@ -144,63 +146,45 @@ thread::scope(|s| { # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -We add two new types to the `std::thread` module: +We add a single new type 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 +struct Scope<'a> {} ``` Next, we need the `scope()` and `spawn()` functions: ```rust -fn scope<'env, F, T>(f: F) -> Result +fn scope<'a, F, T>(f: F) -> Result where - F: FnOnce(&Scope<'env>) -> T; + F: FnOnce(&Scope<'a>) -> T; -impl<'env> Scope<'env> { - fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> +impl<'a> Scope<'a> { + fn spawn(&self, f: F) -> JoinHandle where - F: FnOnce(&Scope<'env>) -> T + Send + 'env, - T: Send + 'env; + F: FnOnce(&Scope<'a>) -> T + Send + 'a, + T: Send + 'a; } ``` -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: +There's just one more thing that will make the API complete: The thread builder +needs to be able to spawn threads inside a scope. ```rust -impl<'scope, T> ScopedJoinHandle<'scope, T> { - fn join(self) -> Result; - fn thread(&self) -> &Thread; -} - impl Builder { - fn spawn_scoped<'scope, 'env, F, T>( - self, - &'scope Scope<'env>, - f: F, - ) -> io::Result> + fn spawn_scoped<'a, F, T>(self, scope: &Scope<'a>, f: F) -> io::Result> where - F: FnOnce(&Scope<'env>) -> T + Send + 'env, - T: Send + 'env; -} + F: FnOnce(&Scope<'a>) -> T + Send + 'a, + T: Send + 'a; ``` -It's also worth pointing out what exactly happens at the scope end when all +Note that this interface is a bit simpler than the one in Crossbeam +because we can now merge `JoinHandle` and `ScopedJoinHandle` into a single type. +Moreover, in Crossbeam, `ScopedJoinHandle` is generic over `'scope`, which is +not really necessary for soundness so we can remove that lifetime to simplify +things further. + +It's also worth discussing what exactly happens at the scope end when all unjoined threads get automatically joined. If all joins succeed, we take the result of the main closure passed to `scope()` and wrap it inside `Ok`. @@ -231,21 +215,35 @@ The main drawback is that scoped threads make the standard library a little bit [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. +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. +* 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. +* 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. -Finally, it's indisputable that 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. +* Implementing scoped threads is very tricky to get right so it's good to have a + reliable solution provided by the standard library. Also, scoped threads in `libstd` + will be simpler because we don't need to introduce a special type for + [scoped join handles](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/struct.ScopedJoinHandle.html) + or [builders](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/struct.ScopedThreadBuilder.html). + +* 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. + +* It's indisputable that 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 From 4158f907a55907624d7cb02c9f1624a65ea6d31a Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Wed, 27 Feb 2019 18:26:59 +0100 Subject: [PATCH 04/10] Expand prior art with some Crossbeam history --- text/0000-scoped-threads.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index 751c6720..19b264ea 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -252,6 +252,30 @@ 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 returns a `thread::Result` rather than `T`. This is because + panics in the old design were just silently ignored, which is not good. + By returning a `Result`, the user can handle panics in whatever way they want. + +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. However, it turns out this is not really necessary for + soundness and was just a conservative safeguard. + 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 almost the same as proposed in this RFC, the only From 8db9e27ad0d73fd74ca52a82e294ac1025afa187 Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Wed, 27 Feb 2019 18:53:18 +0100 Subject: [PATCH 05/10] Add missing closing brace --- text/0000-scoped-threads.md | 1 + 1 file changed, 1 insertion(+) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index 19b264ea..c248eddf 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -176,6 +176,7 @@ impl Builder { where F: FnOnce(&Scope<'a>) -> T + Send + 'a, T: Send + 'a; +} ``` Note that this interface is a bit simpler than the one in Crossbeam From d53412a6e97e3c6b9f0a90923719be19065f2907 Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Wed, 27 Feb 2019 21:33:07 +0100 Subject: [PATCH 06/10] Suggest a threadpool in future possibilites --- text/0000-scoped-threads.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index c248eddf..b172ba4a 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -291,4 +291,5 @@ None. # Future possibilities [future-possibilities]: #future-possibilities -None. +In the future, we could also have a threadpool like Rayon that can spawn +scoped tasks. From c38bca67b014a1e636d0f6250793311d69086a56 Mon Sep 17 00:00:00 2001 From: Stjepan Glavina Date: Sun, 3 Mar 2019 20:47:15 +0100 Subject: [PATCH 07/10] Bring ScopedJoinHandle back --- text/0000-scoped-threads.md | 69 ++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index b172ba4a..8a95d52a 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -41,7 +41,7 @@ Scoped threads in [Crossbeam](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/i 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. +See the [Rationale and alternatives](#rationale-and-alternatives) section for more. # Guide-level explanation [guide-level-explanation]: #guide-level-explanation @@ -146,46 +146,63 @@ thread::scope(|s| { # Reference-level explanation [reference-level-explanation]: #reference-level-explanation -We add a single new type to the `std::thread` module: +We add two new types to the `std::thread` module: ```rust -struct Scope<'a> {} +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<'a, F, T>(f: F) -> Result +fn scope<'env, F, T>(f: F) -> Result where - F: FnOnce(&Scope<'a>) -> T; + F: FnOnce(&Scope<'env>) -> T; -impl<'a> Scope<'a> { - fn spawn(&self, f: F) -> JoinHandle +impl<'env> Scope<'env> { + fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> where - F: FnOnce(&Scope<'a>) -> T + Send + 'a, - T: Send + 'a; + F: FnOnce(&Scope<'env>) -> T + Send + 'env, + T: Send + 'env; } ``` -There's just one more thing that will make the API complete: The thread builder -needs to be able to spawn threads inside a scope. +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; + fn thread(&self) -> &Thread; +} + impl Builder { - fn spawn_scoped<'a, F, T>(self, scope: &Scope<'a>, f: F) -> io::Result> + fn spawn_scoped<'scope, 'env, F, T>( + self, + &'scope Scope<'env>, + f: F, + ) -> io::Result> where - F: FnOnce(&Scope<'a>) -> T + Send + 'a, - T: Send + 'a; + F: FnOnce(&Scope<'env>) -> T + Send + 'env, + T: Send + 'env; } ``` -Note that this interface is a bit simpler than the one in Crossbeam -because we can now merge `JoinHandle` and `ScopedJoinHandle` into a single type. -Moreover, in Crossbeam, `ScopedJoinHandle` is generic over `'scope`, which is -not really necessary for soundness so we can remove that lifetime to simplify -things further. - -It's also worth discussing what exactly happens at the scope end when all +It's also worth pointing out what exactly happens at the scope end when all unjoined threads get automatically joined. If all joins succeed, we take the result of the main closure passed to `scope()` and wrap it inside `Ok`. @@ -229,10 +246,7 @@ several advantages to having them in the standard library: 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. Also, scoped threads in `libstd` - will be simpler because we don't need to introduce a special type for - [scoped join handles](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/struct.ScopedJoinHandle.html) - or [builders](https://docs.rs/crossbeam/0.7.1/crossbeam/thread/struct.ScopedThreadBuilder.html). + reliable solution provided by the standard library. * There are many examples in the official documentation and books that could be simplified by scoped threads. @@ -242,7 +256,7 @@ several advantages to having them in the standard library: This is sometimes a problem in unit tests, where "dangling" threads can accumulate if unit tests spawn threads and forget to join them. -* It's indisputable that users keep asking for scoped threads on IRC and forums +* 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. @@ -274,8 +288,7 @@ There are several differences between old and new scoped threads: non-obvious behavior. 4. `ScopedJoinHandle` got parametrized over `'scope` in order to prevent it from - escaping the scope. However, it turns out this is not really necessary for - soundness and was just a conservative safeguard. + 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 From ab04ad6d5bf5fd1c970fb2954fa145afe5e59c85 Mon Sep 17 00:00:00 2001 From: bstrie <865233+bstrie@users.noreply.github.com> Date: Mon, 19 Jul 2021 19:00:59 -0400 Subject: [PATCH 08/10] Incorporate libs-api team feedback. --- text/0000-scoped-threads.md | 55 +++++++++---------------------------- 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index 8a95d52a..f2e63e62 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -17,8 +17,7 @@ let var = String::from("foo"); thread::scope(|s| { s.spawn(|_| println!("borrowed from thread #1: {}", var)); s.spawn(|_| println!("borrowed from thread #2: {}", var)); -}) -.unwrap(); +}); ``` # Motivation @@ -81,7 +80,7 @@ handle1.join().unwrap(); handle2.join().unwrap(); ``` -Scoped threads coming to the rescue! By opening a new `thread::scope()` block, +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: @@ -99,8 +98,7 @@ thread::scope(|s| { handle1.join().unwrap(); handle2.join().unwrap(); -}) -.unwrap(); +}); ``` That means variables living outside the scope can be borrowed without any @@ -120,13 +118,11 @@ thread::scope(|s| { s.spawn(|_| { println!("thread #2 says: {}", greeting); }); -}) -.unwrap(); +}); ``` -Note that `thread::scope()` returns a `Result` that will be `Ok` if all -automatically joined threads have successfully completed, i.e. they haven't -panicked. +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 @@ -139,8 +135,7 @@ thread::scope(|s| { println!("I belong to the same `thread::scope()` as my parent thread") }); }); -}) -.unwrap(); +}); ``` # Reference-level explanation @@ -165,7 +160,7 @@ inside the scope. The lifetime relations are: Next, we need the `scope()` and `spawn()` functions: ```rust -fn scope<'env, F, T>(f: F) -> Result +fn scope<'env, F, T>(f: F) -> T where F: FnOnce(&Scope<'env>) -> T; @@ -202,28 +197,6 @@ impl Builder { } ``` -It's also worth pointing out what exactly happens at the scope end when all -unjoined threads get automatically joined. If all joins succeed, we take -the result of the main closure passed to `scope()` and wrap it inside `Ok`. - -If any thread panics (and in fact multiple threads can panic), we collect -all those panics into a `Vec`, box it, and finally wrap it inside `Err`. -The error type is then erased because `thread::Result` is just an -alias for: - -```rust -Result> -``` - -This way we can do `thread::scope(...).unwrap()` to propagate all panics -in child threads into the main parent thread. - -If the main `scope()` closure has panicked after spawning threads, we -just resume unwinding after joining child threads. - -Crossbeam's logic for error handling can be found -[here](https://github.com/crossbeam-rs/crossbeam/blob/79210d6ae34a3e84b23546d8abc5c4b81b206019/crossbeam-utils/src/thread.rs#L167-L193). - # Drawbacks [drawbacks]: #drawbacks @@ -276,9 +249,9 @@ Rust 1.0 era. The new one is from the last year's big revamp: There are several differences between old and new scoped threads: -1. `scope()` now returns a `thread::Result` rather than `T`. This is because - panics in the old design were just silently ignored, which is not good. - By returning a `Result`, the user can handle panics in whatever way they want. +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. @@ -292,14 +265,12 @@ There are several differences between old and new scoped threads: 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 almost the same as proposed in this RFC, the only -difference being that `scope()` propagates panics instead of returning `Result`. -This behavior makes more sense for tasks than threads. +threads. Its API is the same as the one proposed in this RFC. # Unresolved questions [unresolved-questions]: #unresolved-questions -None. +Can this concept be extended to async? Would there be any behavioral or API differences? # Future possibilities [future-possibilities]: #future-possibilities From 97915b8eb826172a6de1edc3b7389393ba83424d Mon Sep 17 00:00:00 2001 From: Mara Bos Date: Tue, 4 Jan 2022 16:50:53 +0100 Subject: [PATCH 09/10] Update alternatives section. --- text/0000-scoped-threads.md | 57 +++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 21 deletions(-) diff --git a/text/0000-scoped-threads.md b/text/0000-scoped-threads.md index f2e63e62..e5e52afb 100644 --- a/text/0000-scoped-threads.md +++ b/text/0000-scoped-threads.md @@ -205,33 +205,48 @@ The main drawback is that scoped threads make the standard library a little bit # 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: +* Keep scoped threads in external crates. -* 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. + There are several advantages to having them in the standard library: -* 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. + * 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. -* Implementing scoped threads is very tricky to get right so it's good to have a - reliable solution provided by the standard library. + * 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. -* There are many examples in the official documentation and books that could be - simplified by scoped threads. + * Implementing scoped threads is very tricky to get right so it's good to have a + reliable solution provided by the standard library. -* 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. + * 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. + +* Return a `Result` from `scope` with all the captured panics. + + * This quickly gets complicated, as multiple threads might have panicked. + Returning a `Vec` or other collection of panics isn't always the most useful interface, + and often unnecessary. Explicitly using `.join()` on the `ScopedJoinHandle`s to + handle panics is the most flexible and efficient way to handle panics, if the user wants + to handle them. + +* Don't pass a `&Scope` argument to the threads. + + * `scope.spawn(|| ..)` rather than `scope.spawn(|scope| ..)` would require the `move` keyword + (`scope.spawn(move || ..)`) if you want to use the scope inside that closure, which gets unergonomic. -* 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 From 6de9eb35193852dd910c38f5c2c41e6f3e2a8ed4 Mon Sep 17 00:00:00 2001 From: Mara Bos Date: Sat, 22 Jan 2022 15:53:45 +0100 Subject: [PATCH 10/10] Add issue numbers to scoped threads. --- text/{0000-scoped-threads.md => 3151-scoped-threads.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename text/{0000-scoped-threads.md => 3151-scoped-threads.md} (98%) diff --git a/text/0000-scoped-threads.md b/text/3151-scoped-threads.md similarity index 98% rename from text/0000-scoped-threads.md rename to text/3151-scoped-threads.md index e5e52afb..0c3bfbb0 100644 --- a/text/0000-scoped-threads.md +++ b/text/3151-scoped-threads.md @@ -1,7 +1,7 @@ - Feature Name: scoped_threads - Start Date: 2019-02-26 -- RFC PR: (leave this empty) -- Rust Issue: (leave this empty) +- RFC PR: [rust-lang/rfcs#3151](https://github.com/rust-lang/rfcs/pull/3151) +- Rust Issue: [rust-lang/rust#93203](https://github.com/rust-lang/rust/issues/93203) # Summary [summary]: #summary