-
Notifications
You must be signed in to change notification settings - Fork 7
Implements a more idiomatic thread result without using mem::uninitialized() and the associated undefined behaviour #35
Implements a more idiomatic thread result without using mem::uninitialized() and the associated undefined behaviour #35
Conversation
…inter between the scoped thread and the join handles, that avoids use of mem::ManuallyForget and mem::uninitialized().
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.
Thanks for the PR! Yay, this will finally resolve the remaining safety issues! 👍
I've written some comments inline. There are a few safety issues still left.
But here's a suggestion for an alternative solution: we can eliminate a lot of tricky unsafe code by using Arc<Mutex<Option<T>>>
instead of raw pointers.
First, we change the definition of JoinState
so that result becomes an Arc<Mutex<Option<T>>>
:
struct JoinState<T> {
join_handle: thread::JoinHandle<()>,
result: Arc<Mutex<Option<T>>>,
}
Next, we change join
:
impl<T: Send> JoinState<T> {
fn join(self) -> thread::Result<T> {
self.join_handle
.join()
.map(|_| self.result().lock().unwrap().take().unwrap())
}
}
And finally, the relevant part in spawn
:
let result = Arc::new(Mutex::new(None));
let join_handle = unsafe {
let result = result.clone();
builder_spawn_unchecked(self.builder, move || {
let r = f();
*result.lock().unwrap() = Some(r);
})?
};
That's all. Simple and almost no unsafe code. :)
Now there's some cost due to reference counting and locking, but there's no contention on the mutex and the overall cost is totally negligible.
src/thread.rs
Outdated
fn drop(&mut self) { | ||
let _ = Box::from(self.pointer.as_ptr()); | ||
} | ||
} |
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.
Nit: missing newline.
src/thread.rs
Outdated
impl<T> Drop for ScopedThreadResult<T> { | ||
#[inline] | ||
fn drop(&mut self) { | ||
let _ = Box::from(self.pointer.as_ptr()); |
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.
This line allocates the pointer on the heap. :)
We should use Box::from_raw
instead.
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 that's a stupid mistake on my part, but as you also correctly deduced, Drop
should probably either be scratched or the into_inner()
method should not deallocate the Box
but just deference the pointer and then let the Drop
impl handle the deallocation.
src/thread.rs
Outdated
let mut result = Box::from_raw(result as *mut ManuallyDrop<T>); | ||
*result = ManuallyDrop::new(f()); | ||
mem::forget(result); | ||
thread_result.set(f()); |
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.
If f()
panics, we'll drop the ScopedThreadResult
twice. Once here in this closure and the other time inside .join()
. This is a double-free. Hmmm, we had the same problem in the previous implementation, didn't we? :/
The easiest way of fixing this would probably be to remove the Drop
implementation for ScopedThreadResult
and manually deallocate inside join()
.
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 the current implementation does a double free, due to the use of mem::ManuallyDrop
(I think at least).
src/thread.rs
Outdated
/// implementing Send & Sync. This is only safe to use with scoped threads. | ||
#[derive(Debug)] | ||
struct ScopedThreadResult<T> { | ||
pointer: NonNull<Option<T>> |
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'm pretty sure this needs to be NonNull<UnsafeCell<Option<T>>>
(for the same reason why e.g. Mutex<T>
and RefCell<T>
need UnsafeCell).
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.
No, NonNull is just a wrapper around a pointer, so it can be copied and cloned and doesn't need interior mutability. This is all unsafe though, ofc.
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.
In fact, you were right. By copying the ScopedThreadResult into the thread it also got dropped at the end of the closure, rendering the entire thing unsound. I have implement the safe variant wit Arc and Mutex instead for now and maybe try around a little more with an unsafe variant later.
Will get to the necessary fixes later. There is a lot to be said in favor of safe code over unsafe code, but using an Arc<Mutex<_>> for this feels a bit like overkill if you ask me. |
…t Drop) in favor of a safe solution using std::sync::Arc and std::sync::Mutex
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.
Two unsafe blocks fewer. I like this. 👍
@jeehoonkang What do you think?
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.
Thank you for reporting and fixing a nasty bug! It's also a nice cleanup.
I also agree with @oliver-giersch in that maybe Arc<Mutex<T>>
here is an overkill, but we can discuss optimizations later.
src/thread.rs
Outdated
@@ -380,3 +376,5 @@ impl<'a> Drop for Scope<'a> { | |||
self.drop_all() | |||
} | |||
} | |||
|
|||
type ScopedThreadResult<T> = Arc<Mutex<Option<T>>>; |
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.
Maybe too pedantic to say, but a newline here...? :)
This will need a rebase before merging. |
I will merge the current master into the branch tomorrow and resolve the conflicts. I've noticed that relying on safe code does make it significantly easier to reason about things. I think I have found an unsafe version now that works as well and is reasonably easy to understand, so I might make a PR for that as well.
Scratch that. The original code is a bit contrived but it is well thought through and actually prevents a memory leak in this case.
|
# Conflicts: # src/thread.rs
…' into fix_issue_34_undefined_behaviour # Conflicts: # src/thread.rs
@@ -415,3 +411,5 @@ impl<'env> Drop for Scope<'env> { | |||
self.drop_all().unwrap(); | |||
} | |||
} | |||
|
|||
type ScopedThreadResult<T> = Arc<Mutex<Option<T>>>; |
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.
After inserting a newline here, I think we're ready to merge this PR. (Perhaps we can deal with the newline issue in another PR, too.) @oliver-giersch @stjepang are you okay with merging?
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.
Yes. 👍
Implements a
ScopedThreadResult
struct that stores a raw pointer that can be shared with a scoped thread, while avoidingmem::uninitialized()
andmem::ManuallyForget<T>
.