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:?}");
}
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 = ¬_sync;
std::thread::scope(move |s| {
s.spawn(move || {
shared_ref_to_not_sync.inner.set(41);
});
});
println!("{not_sync:?}");
}
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:?}");
}
(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:
- We have a data type (
Wrapper) which isSendbut!Sync. - We create an exclusive reference to it.
- We send this exclusive reference to another thread.
- 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.
Note that it's also possible to call the
Cell::setmethod, which works on&self. But if you only do shared access, ensure that you mark your closure asmoveclosure, as otherwise the example won't compile (Playground). ↩︎Note that
Sendalso allows something which you can't do withSync-only types: It also allows you to transfer ownership to a different thread. There are certain types which areSyncbut!Send, e.g.MutexGuardinstd. See also this post by @Yandros. ↩︎