Skip to content
This repository has been archived by the owner on Nov 5, 2018. It is now read-only.

Implements a more idiomatic thread result without using mem::uninitialized() and the associated undefined behaviour #35

Merged
5 commits merged into from
Jul 29, 2018
Merged

Implements a more idiomatic thread result without using mem::uninitialized() and the associated undefined behaviour #35

5 commits merged into from
Jul 29, 2018

Conversation

oliver-giersch
Copy link
Contributor

Implements a ScopedThreadResult struct that stores a raw pointer that can be shared with a scoped thread, while avoiding mem::uninitialized() and mem::ManuallyForget<T>.

…inter between the scoped thread and the join handles, that avoids use of mem::ManuallyForget and mem::uninitialized().
@oliver-giersch
Copy link
Contributor Author

oliver-giersch commented Jul 26, 2018

This should solve issue #34 (further discussion in #27).

I am not 100% sure if the Drop implementation for ScopedThreadResult is strictly necessary, as the deferred join() call should deallocate the Box even when a thread panicks.

Copy link

@ghost ghost left a 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());
}
}
Copy link

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());
Copy link

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.

Copy link
Contributor Author

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());
Copy link

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().

Copy link
Contributor Author

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

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

Copy link
Contributor Author

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.

Copy link
Contributor Author

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.

@oliver-giersch
Copy link
Contributor Author

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

@ghost ghost left a 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?

Copy link
Collaborator

@jeehoonkang jeehoonkang left a 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>>>;
Copy link
Collaborator

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...? :)

@ghost
Copy link

ghost commented Jul 27, 2018

This will need a rebase before merging.

@oliver-giersch
Copy link
Contributor Author

oliver-giersch commented Jul 27, 2018

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.
However, unless this brings significant performance improvements it would be hard to argue for replacing perfectly fine safe code with a slightly better performing but much more fragile version.
Perhaps I could try to design some benchmarks to clarify things.

By the way I have done some tests and I am pretty sure that in the current implementation this code leaks memory for the Foo struct. With the Arc<Mutex<_>> this should not be possible.

Scratch that. The original code is a bit contrived but it is well thought through and actually prevents a memory leak in this case.

struct Foo([u8; 4096]);

#[test]
fn leak() {
    scope(|scope| {
        let guard = scope.spawn(|| {
            panic!();
            Foo([0; 4096])
        });
    });
}
```~~

…' 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>>>;
Copy link
Collaborator

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?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. 👍

@ghost ghost merged commit 5380e22 into crossbeam-rs:master Jul 29, 2018
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants