Why is a `&mut` references to a `Send` type not automatically `Sync`?

I attempted to write a demonstration for this:

use std::cell::Cell;

#[derive(Debug)]
struct Wrapper {      // `Send` but `!Sync`
    inner: Cell<i32>, // `Send` but `!Sync`
}

fn main() {
    let mut not_sync = Wrapper { inner: Cell::new(5) };
    let mut_ref_to_not_sync = &mut not_sync;
    std::thread::scope(move |s| {
        s.spawn(move || {
            mut_ref_to_not_sync.inner.set(89);
            // or:
            mut_ref_to_not_sync.inner = Cell::new(90);
        });
    });
    println!("{not_sync:?}");
}

(Playground)

The above example compiles as it's possible to mutate (or just "access" [1]) Wrapper through an exclusive reference in another thread even though Cell<i32> is !Sync. That is because Cell<i32> is Send, and so is Wrapper and &mut Wrapper.

But &Wrapper (opposed to &mut Wrapper) is not Send:

fn main() {
    let not_sync = Wrapper { inner: Cell::new(3) };
    let shared_ref_to_not_sync = &not_sync;
    std::thread::scope(move |s| {
        s.spawn(move || {
            shared_ref_to_not_sync.inner.set(41);
        });
    });
    println!("{not_sync:?}");
}

(Playground)

Errors:

error[E0277]: `Cell<i32>` cannot be shared between threads safely
  --> src/main.rs:12:17

Note further that it's possible to explicitly obtain a &Wrapper in the other thread:

use std::cell::Cell;

#[derive(Debug)]
struct Wrapper {      // `Send` but `!Sync`
    inner: Cell<i32>, // `Send` but `!Sync`
}

fn main() {
    let mut not_sync = Wrapper { inner: Cell::new(5) };
    let mut_ref_to_not_sync = &mut not_sync;
    std::thread::scope(move |s| {
        s.spawn(move || {
            let shared_ref_to_not_sync: &Wrapper = mut_ref_to_not_sync;
            shared_ref_to_not_sync.inner.set(89);
        });
    });
    println!("{not_sync:?}");
}

(Playground)

(This example requires the inner closure to be a move closure to ensure that the &mut Wrapper is actually sent to the other thread. I also added move to the closures in the other examples as well, to avoid confusion.)

So what happens here is:

  1. We have a data type (Wrapper) which is Send but !Sync.
  2. We create an exclusive reference to it.
  3. We send this exclusive reference to another thread.
  4. The other thread can obtain a shared reference from that exclusive reference and operate with it.

Does this make &mut Wrapper "syncish"? Well, yes and no:

"No" because the shared reference to Wrapper is now stuck within the other thread: &Wrapper is !Send + !Sync. Thus only one thread can use it. There is no "sync"ronization.

"Yes" because we didn't send the Wrapper, yet have a &Wrapper in a different thread.

But I guess "syncish" is better worded as "Send" [2], because that's what it is: You can send it (or a mutable reference to it) to another thread, and then work with it (including obtaining shared references to it). But only one thread at a time may use these references.


  1. Note that it's also possible to call the Cell::set method, which works on &self. But if you only do shared access, ensure that you mark your closure as move closure, as otherwise the example won't compile (Playground). ↩︎

  2. Note that Send also allows something which you can't do with Sync-only types: It also allows you to transfer ownership to a different thread. There are certain types which are Sync but !Send, e.g. MutexGuard in std. See also this post by @Yandros. ↩︎