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

For example, the following example does not compile because Cell is not Sync:

use std::cell::Cell;

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

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

However, Cell is Send and since I hold a mutable reference to the Cell I wonder why I can't safely share it?. For example, instead of sharing the mutable reference I could move foo to a different thread, mutate it there, and then receive it back. Something like:

fn on_thread<T: Send>(mut p: T) -> T { p }

fn main() {
    let mut foo: Cell<i32> = Cell::new(1);
    foo = on_thread(foo);
}

Is there a reason why a &mut reference to a Send type is not automatically Sync?

I ran into this issue when writing some async code where holding a reference to a !Sync obj across an await point only works when the obj lives in the same scope as the reference. However, passing the reference to another async function does not work. The reason the first works is that internally the state in the async state machine holds my obj and can send it to another thread where it is then accessed through a reference...

1 Like

Cell is not Sync because you can write to a Cell through a shared reference ("interior mutability", "shared mutability"); if you could send shared references to Cell (i.e. if Cell was Sync), that would enable data races.

A & &mut Cell is coercible to / reborrow-able as a &Cell, so &mut Cell can't be Sync either.

5 Likes

In my case I only have a single mut reference though, so there is only one owner and thus no data races.

Reborrowing would mean I can't access the mut reference while borrowed (and &Cell is still !Sync), so I can't see how this is a problem either. (is & &mut Cell a typo or was the & & intended?).

The & &mut was intentional. Just to make sure we're on the same page --

  • T: Sync if and only if &T: Send
  • You asked why &mut T isn't Sync if T: Send
  • So, you asked why & &mut T isn't Send

And the reason is still that if you could send 2 & &mut Cell to 2 different threads, one can write to the Cell while the other reads the Cell and you have a data race.

Still unclear?

2 Likes

Run this under Miri (under Tools, top right) for example.


Reborrowing would mean I can't access the mut reference while borrowed (and &Cell is still !Sync), so I can't see how this is a problem either. (is & &mut Cell a typo or was the & & intended?).

It's a problem because Cell has interior mutability, so you have to avoid data races through some way other than exclusiveness of &mut. The &mut is somewhat of a red herring here -- the problem is having multiple &Cell on different threads. And & &mut T is effectively a &T as far as these traits go.

3 Likes

As already described, &mut does not provide only exclusive access, but this wrapper does, so it can be always Sync:

That implementation is unstable, but you could write your own version if you want to use it now.

7 Likes

You can tell the compiler that you have exclusive access by using the Cell::get_mut method:

 use std::cell::Cell;
 
 fn ensure_sync<T: Sync>(_: T) {}
 
 fn main() {
     let mut foo: Cell<i32> = Cell::new(1);
-    ensure_sync(&mut foo);
+    ensure_sync(foo.get_mut());
 }

(Playground)

@quinedot yes it seems logical that if &mut T is Sync, & &mut T should be Sync as well. However, what does & &mut mean? isn't it basically just & &? (think you said this before). Don't really see a strong reason why the compiler couldn't figure this out, i.e. make sure that only if I have exclusive access &mut becomes Sync...

@kpreid yes that sounds like exactly what I am trying to tell the compiler, i.e. that I have exclusive access to a Send type which means it is Sync.

@ jbe Cell was just an example. I just needed a type that is Send + !Sync

The thing is that &mut doesn't provide only exclusive access. It provides exclusive access for mutation (or if you like: for operations that require exclusive access), but it can also be turned into multiple shared references. Minimal function demonstrating this:

fn share_from_mut<T: Sync + std::fmt::Display>(input_ref: &mut T) {
    let rref1: &&mut T = &input_ref;
    let rref2: &&mut T = &input_ref;
    std::thread::scope(|s| {
        s.spawn(|| {
            let ref1: &T = &**rref1;
            println!("{ref1}");
        });
        s.spawn(|| {
            let ref2: &T = &**rref2;
            println!("{ref2}");
        });
    });
}

This is reborrowing two &Ts from one &mut T. It's not really necessary to use the double reference &&mut T in a real program, but in this case it makes your proposed change be relevant: If &mut T was Sync even when T isn't, then the &&mut Ts named rref1 and rref2 would be Send, allowing the two spawned threads to get access to &Ts even if T isn't Sync.

(The Exclusive wrapper solves this problem by prohibiting the & reborrowing — you can only get anything out of it if you have &mut access to the Exclusive, thus ensuring that the access is only from a single thread.)

4 Likes

The definition is that if U (&mut T) is Sync, then (and only then) &U (& &mut T) is Send.

It is also true that if U is Sync then &U is also Sync, but I'm not sure what you're getting at.

My playground demonstrates why this is unsound.

You don't just have exclusive access. You can use the &mut to create shared access (either by creating & &mut _ and duplicating that or by shared reborrowing). The key difference between the Exclusive type and &mut is that the former doesn't allow shared access to the wrapped value. &Exclusive is inert.

Or in the words of the documentation,

Indeed, the safety requirements of Sync state that for Exclusive to be Sync, it must be sound to share across threads, that is, it must be sound for &Exclusive to cross thread boundaries. By design, a &Exclusive has no API whatsoever, making it useless, thus harmless, thus memory safe.

& &mut T definitely has an API -- you can get a &T and do everything on T that has a &self API. For example, when it comes to interior mutability (Send + !Sync) types -- you can perform writes.

Thus & &mut [T: Send + !Sync] cannot be Send.

Thus &mut [T: Send + !Sync] cannot be Sync.

1 Like

I think question is: Why does this require that sending the &mut T is unsound, i.e. that &mut T must be !Send if T: Send + !Sync? (edit: it does not; see P.S. below) Wouldn't it be sufficient if the &&mut T was !Send + !Sync? Because then the reborrow (&&mut T) would be "stuck" to one thread only, thus no race conditions are possible.

Anyway, I didn't fully read/understand everything in this thread yet, so no need to explain to me right now. I'll first try to understand better what's going on.

P.S.: I just saw the OP asked why the &mut T can't be Sync. Of course it can't be Sync because then &&mut T would be Send by definition. Sorry for my confusion. (&mut T is indeed Send, see Playground)

1 Like

In short, I would explain it like this:

If &mut T was Sync (but T: !Sync), then &&mut T would be Send + Copy by definition (because X: Sync implies &X: Send and shared references are Copy).

If &&mut T can be copied and sent to numerous threads, then several threads could invoke methods that take &self (i.e. which work on &T). This requires T being synchronized (but we said T: !Sync, which is contradiction).

No problem in that. That's why we usually leave such deductions to the compiler.[1] Imagine the horror of having to do such reasoning every day in your head,[2] without any help, just to make sure your programs behave correctly (and better don't mess up) — wait.. no need to imagine, since other programming languages exist :grin:


  1. Or for safely wrapped usage code, to experienced Rustaceans who take some time to check and double check that they set all their Send and Sync implementations correctly. Even then though, imagine how hard it would be (and how often people would do a bad job) to document the precise requirements of how a type/API can be used in a thread-safe manner, without such clearly defined marker traits. ↩︎

  2. or on paper? ↩︎

4 Likes

Thank you for your replies this all makes sense! My question is a bit hypothetical since its obviously doesn't work with the current rules.

& &mut T definitely has an API -- you can get a &T and do everything on T that has a &self API. For example, when it comes to interior mutability (Send + !Sync) types -- you can perform writes.

This is basically what I meant, the mut in & &mut T is a bit misleading since you can't mutate anything in & &mut T because it's effectively a & &T, isn't it?. So, hypothetically, if the compiler would derive &mut T is Sync if T is Send it could also figure out that & &mut T is effectively & &T which isn't Sync. In the fn share_from_mut() example from above this means you couldn't share rref1 and rref2 with a different thread.

You can probably poke holes in this ad hoc definition, but the basic idea is that the compiler already knows when you have exclusive access, i.e. when you can mutate a type, and if you have exclusive access then you should be able to send an exclusive reference to another thread which in turn makes &mut T Syncish...

The whole point of "Foo is Sync" is that it means that &Foo can be sent between threads. If you replace Foo with &mut T, this simply results in the type & &mut T, there's nothing special going on.

And you cannot possibly put in an exception either:

&mut T can be used e. g. to instantiate generic arguments of a function. If you have a generic function fn f<Foo: Sync>(x: &Foo, cb: fn(&Foo)), the compiler accepts if you spawn new threads inside this generic function, and call the callback there. However foo can then be called with Foo = &mut T, and if &mut T is sync, you quickly get the soundness issues already discussed above, without any chance for the compiler to magically special-case & &mut T to be not Send, as all it sees during type checking is the generic parameter Foo.

There's also the case of other types that work analogously to &_ shared references. E. g. just like & &mut T, the type Arc<&mut T> must also enforce that &mut T is Sync (and Send) for the whole Arc to become Send, so. special casing just & &mut T isn't sufficient either.

This doesn't make &mut T any Syncish, but it does make it Send. Which is already a thing, and is a power &T doesn't have. Since T: Send is (in most cases) the weaker requirement than T: Send T: Sync, the fact that &T: Send requires T: Sync but &mut T: Send requires T: Send should, as far as I'm aware, already sufficiently capture this idea how exclusive access helps with thread safety.

6 Likes

One of these Sends was intended to be Sync, I guess?

1 Like

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. ↩︎

1 Like

Two observations here:

  • if you have a T you own you also have exclusive access to it. This doesn't make every T: Send also T: Sync though

  • You can send &mut T to another thread if T: Send, this doesn't make it Sync. To be Sync you must be able to send a &&mut T to another thread. The whole point of Sync is to be a "shortcut" to writing for<'a> &'a U: Send, so it follows that if you can't send a &&mut T to another thread if T: !Sync then &mut T should also be !Sync. If it was Sync you would only lose consistency in the trait system, without gaining any ability.

2 Likes

Right, but maybe it's worth noting that T: Send + !Sync allows sending the &mut T and then create the &&mut T in the other thread (Playground, similar to my last one, but creating a &&mut).

You can do the same by sending the T to the other thread and create a &T there, there's nothing special about &mut T here.

Also note that this way the shared reference is only accessible from a single thread at any point in time, while the important detail behind "sending &&mut T to another thread" is that the current thread is still able to retain an &&mut T, thus creating a situation where multiple threads have shared access. In your example instead only one thread is ever able to access the T through a shared reference.