Skip to content
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

Add example of thinking about Send/Sync's soundness #259

Merged
merged 21 commits into from
Apr 1, 2021
Merged
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions src/send-and-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,171 @@ of their pervasive use of raw pointers to manage allocations and complex ownersh
Similarly, most iterators into these collections are Send and Sync because they
largely behave like an `&` or `&mut` into the collection.

## Example

[`Box`][box-doc] is implemented as it's own special intrinsic type by the
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
compiler for [various reasons][box-is-special], but we can implement something
with similar-ish behavior ourselves to see an example of when it is sound to
implement Send and Sync. Let's call it a `Carton`.

We start by writing code to take a value allocated on the stack and transfer it
to the heap.

```rust,ignore
use std::mem::size_of;
use std::ptr;

dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
struct Carton<T>(ptr::NonNull<T>);

impl<T> Carton<T> {
pub fn new(value: T) -> Self {
// Allocate enough memory on the heap to store one T.
assert_ne!(size_of::<T>(), 0, "Zero-sized types are out of the scope of this example);
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
let memptr: *mut c_void = ptr::null_mut();
unsafe {
let ret = libc::posix_memalign(
&mut memptr,
align_of::<T>(),
size_of::<T>()
);
assert_eq!(ret, 0, "Failed to allocate or invalid alignment");
};

// NonNull is just a wrapper that enforces that the pointer isn't null.
let mut ptr = unsafe {
// Safety: memptr is dereferenceable because we created it from a
// reference and have exclusive access.
NonNull::new(memptr.cast::<T>())
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
.expect("Guaranteed non-null if posix_memalign returns 0")
};

// Move value from the stack to the location we allocated on the heap.
unsafe {
// Safety: If non-null, posix_memalign gives us a ptr that is valid
// for writes and properly aligned.
ptr.as_ptr().write(value);
}

Self(ptr)
}
}
```

This isn't very useful, because once our users give us a value they have no way
to access it. [`Box`][box-doc] implements [`Deref`][deref-doc] and
[`DerefMut`][deref-mut-doc] so that you can access the inner value. Let's do
that.

```rust
use std::ops::{Deref, DerefMut};

# struct Carton<T>(std::ptr::NonNull<T>);

dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
impl<T> Deref for Carton<T> {
type Target = T;

fn deref(&self) -> &Self::Target {
unsafe {
// Safety: The pointer is aligned, initialized, and dereferenceable
// by the logic in [`Self::new`]. We require writers to borrow the
// Carton, and the lifetime of the return value is elided to the
// lifetime of the input. This means the borrow checker will
// enforce that no one can mutate the contents of the Carton until
// the reference returned is dropped.
self.0.as_ref()
}
}
}

impl<T> DerefMut for Carton<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe {
// Safety: The pointer is aligned, initialized, and dereferenceable
// by the logic in [`Self::new]. We require writers to mutably
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
// borrow the Carton, and the lifetime of the return value is
// elided to the lifetime of the input. This means the borrow
// checker will enforce that no one else can access the contents
// of the Carton until the mutable reference returned is dropped.
self.0.as_mut()
}
}
}
```

Finally, lets think about whether our `Carton` is Send and Sync. Something can
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
safely be Send unless it shares mutable state with something else without
enforcing exclusive access to it. Each `Carton` has a unique pointer, so
we're good.

```rust
# struct Carton<T>(std::ptr::NonNull<T>);
// Safety: No one besides us has the raw pointer, so we can safely transfer the
// Carton to another thread if T can be safely transferred.
unsafe impl<T> Send for Carton<T> where T: Send {}
Copy link
Contributor

Choose a reason for hiding this comment

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

Right now Carton<T> does not implement Drop, so there is a memory leak. Once it does (with a call to libc::free(self.0.as_ptr().cast());, part of the requirements of Send would be that free itself can be called on a pointer that yielded by an allocation done on another thread. This is the case of free, so we may forget to mention it, but it's a requirement nonetheless.

A nice example where this does not happen is with a MutexGuard: notice how it is not Send. Indeed, the Rust implementation of mutexes make use of functions (`pthread_ that explicitly mention the handles they yield cannot be used from another thread, even if the access is unique.

Copy link
Contributor Author

@dzfranklin dzfranklin Mar 23, 2021

Choose a reason for hiding this comment

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

That's a really good explanation. I tweaked it a little and added it to the page (a22d055).

```

What about Sync? For `Carton` to be Sync we have to enforce that you can't
write to something stored in a `&Carton` while that same something could be read
or written to from another `&Carton`. Since you need an `&mut Carton` to
write to the pointer, and the borrow checker enforces that mutable
references must be exclusive, there are no soundness issues making `Carton`
sync either.

```rust
# struct Carton<T>(std::ptr::NonNull<T>);
// Safety: Since there exists a public way to go from a `&Carton<T>` to a `&T`
// in an unsynchronized fashion (such as `Deref`), then `Carton<T>` can't be
// `Sync` if `T` isn't.
// Conversely, `Carton` itself does not use any interior mutability whatsoever:
// all the mutations are performed through an exclusive reference (`&mut`). This
// means it suffices that `T` be `Sync` for `Carton<T>` to be `Sync`:
unsafe impl<T> Sync for Carton<T> where T: Sync {}
```

When we assert our type is Send and Sync we usually need to enforce that every
contained type is Send and Sync. When writing custom types that behave like
standard library types we can assert that we have the same requirements.
For example, the following code asserts that a Carton is Send if the same
sort of Box would be Send, which in this case is the same as saying T is Send.

```rust
# struct Carton<T>(std::ptr::NonNull<T>);
unsafe impl<T> Send for Carton<T> where Box<T>: Send {}
```

Right now Carton<T> has a memory leak, as it never frees the memory it allocates.
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
Once we fix that we have a new requirement we have to ensure we meet to be Send:
we need to know `free` can be called on a pointer that was yielded by an
allocation done on another thread. We can check this is true in the docs for
[`libc::free`][libc-free-docs].

```rust,ignore
dzfranklin marked this conversation as resolved.
Show resolved Hide resolved
impl<T> Drop for Carton<T> {
fn drop(&mut self) {
unsafe {
libc::free(self.0.as_ptr().cast());
}
}
}
```

A nice example where this does not happen is with a MutexGuard: notice how
[it is not Send][mutex-guard-not-send-docs-rs]. The implementation of MutexGuard
[uses libraries][mutex-guard-not-send-comment] that require you to ensure you
don't try to free a lock that you acquired in a different thread. If you were
able to Send a MutexGuard to another thread the destructor would run in the
thread you sent it to, violating the requirement. MutexGuard can still be Sync
because all you can send to another thread is an `&MutexGuard` and dropping a
reference does nothing.
Copy link
Contributor

Choose a reason for hiding this comment

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

👌


TODO: better explain what can or can't be Send or Sync. Sufficient to appeal
only to data races?

[unsafe traits]: safe-unsafe-meaning.html
[box-doc]: https://doc.rust-lang.org/std/boxed/struct.Box.html
[box-is-special]: https://manishearth.github.io/blog/2017/01/10/rust-tidbits-box-is-special/
[deref-doc]: https://doc.rust-lang.org/core/ops/trait.Deref.html
[deref-mut-doc]: https://doc.rust-lang.org/core/ops/trait.DerefMut.html
[mutex-guard-not-send-docs-rs]: https://doc.rust-lang.org/std/sync/struct.MutexGuard.html#impl-Send
[mutex-guard-not-send-comment]: https://github.com/rust-lang/rust/issues/23465#issuecomment-82730326
[libc-free-docs]: https://linux.die.net/man/3/free