An unsafe tour of Rust's Send and Sync

Rust's concurrency safety is based around the Send and Sync traits. For people writing safe code, you don't really need to understand these traits on a deep level, only enough to satisfy the compiler when it spits errors at you (or switch from std threads to Crossbeam scoped threads to make errors go away). However if you're writing unsafe concurrent code, such as having a &UnsafeCell<T> hand out &T and &mut T, you need to understand Send and Sync at a more fundamental level, to pick the appropriate trait bounds when writing unsafe impl Send/Sync statements, or add the appropriate PhantomData<T> to your types.

In this article, I will explore the precise behavior of Send and Sync, and explain why the standard library's trait bounds are the way they are.

🔗 Prior art

You can think of Send as "Exclusive access is thread-safe," and Sync as "Shared access is thread-safe."

[Source]

I recommended first reading "Rust: A unique perspective". This article gives a conceptual overview of the mechanics (unique and shared references) I will analyze in more depth.

🔗 Defining Sync and Send

T: Send means T and &mut T (which allow dropping T) can be passed between threads. T: Sync means &T (which allows shared/aliased access to T) can be passed between threads. Either or both may be true for any given type. T: Sync&T: Send (by definition).

One way that T: !Sync can occur is if a type has non-atomic interior mutability. This means that every &T (there can be more than one) can mutate T at the same time non-atomically, causing data races if a &T is sent to another thread. T: !Sync includes Cell<V> and RefCell<V>, as well as Rc<V> (which acts like &(Cell<RefCount>, V)).

T: !Send if a type is bound to the current thread. Examples:

🔗 Primitives

Most primitive types (like i32) are Send+Sync. They can be read through shared references (&) by multiple threads at once (Sync), and modified through unique references (&mut) by any one thread at a time (Send).

🔗 Owning references

Box<T> and &mut T give the same access as having a T directly, so it shares the same Sync/Send status as T.

(Sidenote) Technically, &mut T allows swapping the T (which cannot panic), but prohibits moving the T. This is because moving invalidates the &mut T, and the &mut Ts and T it's constructed from.

For a demonstration of &mut T, see "Example: Passing &mut (T: Send) between threads" section in this page.

🔗 Where these semantics are defined

🔗 Shared references

Unlike owning references, many &T can be created from the same T. And an unlimited number of &T and Rc<T> and Arc<T> copies/clones can point to the same T.

By definition, you can Send &T instances to other threads iff T is Sync. For example, &i32 is Send because i32 is Sync.

Less obvious is that &T: Sync requires that T: Sync. Why is this the case?

🔗 Sources

🔗 Interior mutability

Cell<i32> (and RefCell<i32>) is !Sync because it has single-threaded interior mutability, which translates to multithreaded data races.

UnsafeCell<i32> is !Sync to prevent misuse, since only some usages are Sync and impl !Sync is unstable. As a result, you need to unsafe impl Sync (which shows up in grep) if you want concurrent access.

🔗 Smart pointers: Rc<T>

Rc<i32> acts like &(Cell<RefCount>, i32). It is !Sync because Cell<RefCount> has interior mutability and data races on RefCount, and !Send because Rc<T> is clonable, acts like a&Cell<RefCount>, and Cell<RefCount> is !Sync.

(Technically Rc<i32> also acts like &mut T in its ability to drop T, but it doesn't matter because it's always !Send and !Sync.)

🔗 Sources

🔗 Smart pointers: Arc<T> (atomic refcounting)

Arc<T> is a doozy. It acts like &(Atomic<RefCount>, T) in its ability to alias T, and T/&mut T in its ability to drop or get_mut or try_unwrap the T.

Because &T can alias, Arc<T>: Send+Sync requires T: Sync.

Additionally, Arc<T>: Send requires T: Send (because you can move Arc<T> across threads, and T with it).

And Arc<T>: Sync requires T: Send, because if T: !Send but Arc<T>: Sync, you could clone the Arc (via &Arc<T>) from another thread, and drop (or get_mut or try_unwrap) the clone last, violating T: !Send.

(Atomic<RefCount> is Send+Sync and does not contribute to Arc's thread safety.)

🔗 Sources

This was also discussed in a Stack Overflow question.

🔗 Mutexes

Mutex<T> is Sync even if T isn't, because if multiple threads obtain &Mutex<T>, they can't all obtain &T.

Mutex<T>: Sync requires T: Send. We want &Mutex to be Send, meaning multiple threads can lock the mutex and obtain a &mut T (which lets you swap T and control which thread calls Drop). To hand-wave, exclusive access to T gets passed between threads, requiring that T: Send.

Mutex<T>: Send requires T: Send because Mutex is a value type.

MutexGuard<T> is !Send because it's bound to the constructing thread (on some OSes including Windows, you can't send or exchange "responsibility for freeing a mutex" to another thread). Otherwise it acts like a &mut T, which is Sync if T is Sync. Additionally you can extract a &mut T (which is Send) using &mut *guard.

🔗 Sources

🔗 Contrived corner cases

Mutex<MutexGuard<i32>> is !Sync because MutexGuard<i32> is !Send.

🔗 Thoughts on trait bounds and flexibility for users

Why does Arc<T> not have a where T: Send + Sync trait bound, but instead allows you to construct Arc<T> for any T (but just not send/share it across threads)?

I've heard that you should avoid putting trait bounds in types, but (if I remember correctly) instead in method implementations, or (in the case of Arc) in conditional Send/Sync implementations. One person said:

The reason the restrictions are usually on the implementations rather than on the type in general is that you don't usually know every possible implementation If you later realize you can add other functionality, you can just add additional impl blocks with different restrictions, whereas if they were on the type you would potentially have to worry about unifying the restrictions (which can be really awkward) or removing them altogether

When asking about this topic, I was pointed to the Rust API guidelines, but I couldn't find any discussion of this issue.


I personally encountered this topic when I used an Arc internally for the flip-cell crate (which turns out to be equivalent to Oddio's Swap type and the triple-buffer crate).

Arc<T>: Sync is only safe if T: Send, not just T: Sync; this is because another thread can look at an &Arc<T>, clone it, and obtain an Arc<T> sharing ownership over the same object. But if we create a type FlipReader<T> (source) which contains an Arc<Wrapper<T>> but prohibits cloning it, then making FlipReader<T>: Sync does not allow another thread to take shared ownership of Wrapper<T>, so the Wrapper<T>: Send trait bound is unnecessary.

Had the struct Arc<T> required T: Send + Sync to even be constructed, Arc would be crippled as a building block for unsafe code.

🔗 Example: Passing &mut (T: Send) between threads

Cell is Send but not Sync. Both Cell and &mut Cell can be passed between threads. The following code builds as-is, but not if &mut is changed to &.

use std::thread;
use std::cell::Cell;

fn main() {
    // Send + !Sync
    let cell_ref: &mut Cell<i32> = Box::leak(Box::new(Cell::new(0)));

    thread::spawn(move || {
        cell_ref.replace(1);
    });
}

🔗 Example: &T: Send or Sync both depend on T: Sync

If T: !Sync (for example Cell), then &T is neither Send nor Sync.

use std::cell::Cell;

fn ensure_sync<T: Sync>(_: T) {}
fn ensure_send<T: Send>(_: T) {}

fn main() {
    let foo = Cell::new(1i32);
    ensure_sync(&foo);
    ensure_send(&foo);
}

Trying to compile this code returns the errors:

Standard Error

   Compiling playground v0.0.1 (/playground)
error[E0277]: `Cell<i32>` cannot be shared between threads safely
 --> src/main.rs:8:17
  |
3 | fn ensure_sync<T: Sync>(_: T) {}
  |                   ---- required by this bound in `ensure_sync`
...
8 |     ensure_sync(&foo);
  |                 ^^^^ `Cell<i32>` cannot be shared between threads safely
  |
  = help: within `&Cell<i32>`, the trait `Sync` is not implemented for `Cell<i32>`
  = note: required because it appears within the type `&Cell<i32>`

error[E0277]: `Cell<i32>` cannot be shared between threads safely
 --> src/main.rs:9:17
  |
4 | fn ensure_send<T: Send>(_: T) {}
  |                   ---- required by this bound in `ensure_send`
...
9 |     ensure_send(&foo);
  |                 ^^^^ `Cell<i32>` cannot be shared between threads safely
  |
  = help: the trait `Sync` is not implemented for `Cell<i32>`
  = note: required because of the requirements on the impl of `Send` for `&Cell<i32>`

If T: !Send + Sync (for example MutexGuard), then &T is still Send + Sync. (This makes sense, because T: !Send only constrains the behavior of a &mut T, and should not affect the properties of a &T.)

use std::marker::PhantomData;
use std::sync::MutexGuard;

fn ensure_sync<T: Sync>(_: T) {}
fn ensure_send<T: Send>(_: T) {}

fn main() {
    let foo = PhantomData::<MutexGuard<i32>> {};
    ensure_sync(&foo);
    ensure_send(&foo);
}

This blog post was edited on 2021-02-09 to fix minor errors and clarify Rc<V>.