-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove argument from closure in thread::Scope::spawn. #94559
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -9,23 +9,24 @@ use crate::sync::Arc; | |||||
/// A scope to spawn scoped threads in. | ||||||
/// | ||||||
/// See [`scope`] for details. | ||||||
pub struct Scope<'env> { | ||||||
pub struct Scope<'scope, 'env: 'scope> { | ||||||
data: ScopeData, | ||||||
/// Invariance over 'env, to make sure 'env cannot shrink, | ||||||
/// Invariance over 'scope, to make sure 'scope cannot shrink, | ||||||
/// which is necessary for soundness. | ||||||
/// | ||||||
/// Without invariance, this would compile fine but be unsound: | ||||||
/// | ||||||
/// ```compile_fail | ||||||
/// ```compile_fail,E0373 | ||||||
/// #![feature(scoped_threads)] | ||||||
/// | ||||||
/// std::thread::scope(|s| { | ||||||
/// s.spawn(|s| { | ||||||
/// s.spawn(|| { | ||||||
/// let a = String::from("abcd"); | ||||||
/// s.spawn(|_| println!("{:?}", a)); // might run after `a` is dropped | ||||||
/// s.spawn(|| println!("{:?}", a)); // might run after `a` is dropped | ||||||
/// }); | ||||||
/// }); | ||||||
/// ``` | ||||||
scope: PhantomData<&'scope mut &'scope ()>, | ||||||
env: PhantomData<&'env mut &'env ()>, | ||||||
} | ||||||
|
||||||
|
@@ -88,12 +89,12 @@ impl ScopeData { | |||||
/// let mut x = 0; | ||||||
/// | ||||||
/// thread::scope(|s| { | ||||||
/// s.spawn(|_| { | ||||||
/// s.spawn(|| { | ||||||
/// println!("hello from the first scoped thread"); | ||||||
/// // We can borrow `a` here. | ||||||
/// dbg!(&a); | ||||||
/// }); | ||||||
/// s.spawn(|_| { | ||||||
/// s.spawn(|| { | ||||||
/// println!("hello from the second scoped thread"); | ||||||
/// // We can even mutably borrow `x` here, | ||||||
/// // because no other threads are using it. | ||||||
|
@@ -109,7 +110,7 @@ impl ScopeData { | |||||
#[track_caller] | ||||||
pub fn scope<'env, F, T>(f: F) -> T | ||||||
where | ||||||
F: FnOnce(&Scope<'env>) -> T, | ||||||
F: for<'scope> FnOnce(&'scope Scope<'scope, 'env>) -> T, | ||||||
{ | ||||||
let scope = Scope { | ||||||
data: ScopeData { | ||||||
|
@@ -118,6 +119,7 @@ where | |||||
a_thread_panicked: AtomicBool::new(false), | ||||||
}, | ||||||
env: PhantomData, | ||||||
scope: PhantomData, | ||||||
}; | ||||||
|
||||||
// Run `f`, but catch panics so we can make sure to wait for all the threads to join. | ||||||
|
@@ -138,7 +140,7 @@ where | |||||
} | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unrelated to this PR, but it seems worrying that if park() panics we get unsoundness -- it seems like we should be using a drop guard or similar rather than catch_unwind? (On non-futex or windows platforms, the generic parker implementation has some clear panic points, though they should not happen in theory). But maybe that kind of defensiveness isn't worthwhile -- anyway, just a passing thought as I wrap my head around the impl here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should probably just change those implementations and guarantee that |
||||||
} | ||||||
|
||||||
impl<'env> Scope<'env> { | ||||||
impl<'scope, 'env> Scope<'scope, 'env> { | ||||||
/// Spawns a new thread within a scope, returning a [`ScopedJoinHandle`] for it. | ||||||
/// | ||||||
/// Unlike non-scoped threads, threads spawned with this function may | ||||||
|
@@ -163,10 +165,10 @@ impl<'env> Scope<'env> { | |||||
/// to recover from such errors. | ||||||
/// | ||||||
/// [`join`]: ScopedJoinHandle::join | ||||||
pub fn spawn<'scope, F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> | ||||||
pub fn spawn<F, T>(&'scope self, f: F) -> ScopedJoinHandle<'scope, T> | ||||||
where | ||||||
F: FnOnce(&Scope<'env>) -> T + Send + 'env, | ||||||
T: Send + 'env, | ||||||
F: FnOnce() -> T + Send + 'scope, | ||||||
T: Send + 'scope, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It shouldn't be needed for the return value to be allowed to be as small as
Suggested change
mainly, I'd find it less problematic if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do you have an example of any difference that this change makes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that was unexpected; it looks like the edition-2021 magic fails when the capture is used "by value" but with fully inferred types (the Thus, using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it a problem if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 It all boils down to what does the "scoped runtime" do with the returned value when not joined / and potentially when leaked —I suspect the latter is the only actual area of problems, and the current implementation leaks stuff in that case, so at first glance it looks like it's gonna be fine.
To recap:
So, one thing that can be done is sticking to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I spent some more time thinking about this, and concluded that using #![feature(scoped_threads)]
use std::{
thread::{scope, sleep},
time::Duration,
};
fn main() {
let mut a = "asdf".to_string();
let r = &a;
scope(|s| {
s.spawn(move || {
scopeguard::guard(s, move |s| {
sleep(Duration::from_millis(100));
s.spawn(move || dbg!(r));
})
});
});
drop(a);
sleep(Duration::from_millis(200));
} With std from this PR:
Eep! However! After thinking a bit more, I concluded that using fn main() {
let mut a = "asdf".to_string();
let r = &a;
scope(|s| {
s.spawn(move || {
scopeguard::guard(r, move |r| {
sleep(Duration::from_millis(100));
dbg!(r);
})
});
});
drop(a);
sleep(Duration::from_millis(200));
} On Rust rustc 1.61.0-nightly (8769f4e 2022-03-02):
This makes me think that it's unlikely that an implementation would be unsound with the (The fix is to drop the return value before marking a thread as joined. So, an implementation issue, not an API issue. I'll send a PR for that.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch! 🙂 So, I've been thinking about this, to fully spell out what my hunch / feeling of uneasyness was. Let's start with the following new names, for the remainder of this post:
So from a start point of - F : 'cannot_capture_inside_scope + FnOnce(ScopedSpawner...) -> T,
+ F : 'cannot_capture_inside_scope_except_spawner + FnOnce() -> T, // equivalent API
Now, you are also giving the - T : 'cannot_capture_inside_scope,
+ T : 'cannot_capture_inside_scope_except_spawner, // new capability
So what granting it the
With the caveat that mistakes here endanger soundness, and that removing this
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I wonder if there's any reasonable implementation where this requires special attention. The point I was trying to make towards the end of my last comment is that implementations need to take care of handling Drop implementations that borrow from I don't think there is really any difference between spawning threads near the end of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah I guess I didn't get it 100% the first time 😅; I remained convinced it would lead to a more subtle approach, but as you've patiently mentioned, the subtlety is already there even with Thanks for having put up with my initial skepticism! 🙏 Silly GH won't let me mark my own remark as resolved 🤦 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a great way of putting it. |
||||||
{ | ||||||
Builder::new().spawn_scoped(self, f).expect("failed to spawn thread") | ||||||
} | ||||||
|
@@ -196,7 +198,7 @@ impl Builder { | |||||
/// thread::scope(|s| { | ||||||
/// thread::Builder::new() | ||||||
/// .name("first".to_string()) | ||||||
/// .spawn_scoped(s, |_| | ||||||
/// .spawn_scoped(s, || | ||||||
/// { | ||||||
/// println!("hello from the {:?} scoped thread", thread::current().name()); | ||||||
/// // We can borrow `a` here. | ||||||
|
@@ -205,7 +207,7 @@ impl Builder { | |||||
/// .unwrap(); | ||||||
/// thread::Builder::new() | ||||||
/// .name("second".to_string()) | ||||||
/// .spawn_scoped(s, |_| | ||||||
/// .spawn_scoped(s, || | ||||||
/// { | ||||||
/// println!("hello from the {:?} scoped thread", thread::current().name()); | ||||||
/// // We can even mutably borrow `x` here, | ||||||
|
@@ -222,14 +224,14 @@ impl Builder { | |||||
/// ``` | ||||||
pub fn spawn_scoped<'scope, 'env, F, T>( | ||||||
self, | ||||||
scope: &'scope Scope<'env>, | ||||||
scope: &'scope Scope<'scope, 'env>, | ||||||
f: F, | ||||||
) -> io::Result<ScopedJoinHandle<'scope, T>> | ||||||
where | ||||||
F: FnOnce(&Scope<'env>) -> T + Send + 'env, | ||||||
T: Send + 'env, | ||||||
F: FnOnce() -> T + Send + 'scope, | ||||||
T: Send + 'scope, | ||||||
{ | ||||||
Ok(ScopedJoinHandle(unsafe { self.spawn_unchecked_(|| f(scope), Some(&scope.data)) }?)) | ||||||
Ok(ScopedJoinHandle(unsafe { self.spawn_unchecked_(f, Some(&scope.data)) }?)) | ||||||
} | ||||||
} | ||||||
|
||||||
|
@@ -245,7 +247,7 @@ impl<'scope, T> ScopedJoinHandle<'scope, T> { | |||||
/// use std::thread; | ||||||
/// | ||||||
/// thread::scope(|s| { | ||||||
/// let t = s.spawn(|_| { | ||||||
/// let t = s.spawn(|| { | ||||||
/// println!("hello"); | ||||||
/// }); | ||||||
/// println!("thread id: {:?}", t.thread().id()); | ||||||
|
@@ -279,7 +281,7 @@ impl<'scope, T> ScopedJoinHandle<'scope, T> { | |||||
/// use std::thread; | ||||||
/// | ||||||
/// thread::scope(|s| { | ||||||
/// let t = s.spawn(|_| { | ||||||
/// let t = s.spawn(|| { | ||||||
/// panic!("oh no"); | ||||||
/// }); | ||||||
/// assert!(t.join().is_err()); | ||||||
|
@@ -299,7 +301,7 @@ impl<'scope, T> ScopedJoinHandle<'scope, T> { | |||||
} | ||||||
} | ||||||
|
||||||
impl<'env> fmt::Debug for Scope<'env> { | ||||||
impl fmt::Debug for Scope<'_, '_> { | ||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||||||
f.debug_struct("Scope") | ||||||
.field("num_running_threads", &self.data.num_running_threads.load(Ordering::Relaxed)) | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to see the documentation expanded here as to the meaning of 'scope and 'env, likely drawing from the commentary on #93203 (comment) and this PR.
Part of me wants to say that we should also add some kind of user-visible note about 'scope/'env -- it seems like this is an API that we would like new users to Rust to go "cool!" about, and I think the current signature will make them afraid that all work in Rust with threads requires complicated lifetime signatures that look pretty opaque -- even to experienced Rustaceans, I suspect :) I'm not sure if putting the note on this type and then linking to it from the other APIs which use Scope is right -- that would be my first instinct, but the more complicated signature is on those APIs (with
for<'scope>
... and all).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we probably should, but I'm not sure how yet. I've added it to the tracking issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, I have an idea of renaming stuff that could improve stuff, but I was gonna officially suggest it in a follow-up PR, but the gist of it would be:
thread::scope()
thread::scoped()
or
thread::with_scope()
'env
'scope
'scope
'spawner
Scope
ScopedSpawner
or just
Spawner
Funnily enough, by phrasing the
F : 'spawner
bound as the straightforward "it may be used / may be running while theScopedSpawner
is alive"1, it turns out we don't need to think about the universal quantification of'scope : 'env
and all those more complicated considerations. We just have a thread that has to be able to be run(ning) / used while theScopedSpawner
, the handle that ensures the thread is joined, is itself dropped.It is really the main key point.
Then, and only then, you may consider going further in the reasoning:
We can see that:
is not that different from the more natural:
Why "natural"
Indeed, if we stare at the latter, it's actually the pre-leakpocalypse API.
And the bound / logic itself, modulo
Send
, is actually the same as imagining a collection of drop glues / ofdefer
s, since joining a thread on drop is not that different from running that thread's logic directly on drop:We "just" have to add that extra "upper bound" on
'spawner
for convenience, since a post-leakpocalypse API needs a higher-order lifetime, and a higher-order lifetime, by default, not only expresses a local/caged lifetime, but it also covers the range of'static
or other big lifetimes (our spawner won't be living for that long!). Hence the need to opt-out of that "range of big lifetimes", and use afor<'spawner where 'spawner : 'scope>
quantification. Thatwhere
clause can't be written there and then in current Rust, so it is written insideScopedSpawner
's definition, which thus needs to carry that otherwise useless extra lifetime parameter.Footnotes
which is actually almost exactly what happens –just a bit too conservative, since there is a tiny instant between the moment where all the threads are joined and their non-collected computed values are dropped and the moment the
ScopedSpawner
itself finally dies ↩There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should rename Scope to Spawner, because I imagine Scope might be able to do more than just spawning threads in the future. For example, I can imagine something like
scope.is_panicked()
to check if any of the threads panicked. Orscope.num_running_threads()
to see how many threads are currently still running in this scope. Etc.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But let's have that discussion on the tracking issue. ^^