How is that possible that there is a Send implementation but not Sync? Isn't it the same?
The officuial documentation for Sync states type T is Sync if and only if &T is Send
Here is an example of what confuses me
How is that possible that there is a Send implementation but not Sync? Isn't it the same?
The officuial documentation for Sync states type T is Sync if and only if &T is Send
Here is an example of what confuses me
One example is if you use unsynchronized interior mutability. For example both Cell
and RefCell
implement Send
but not Sync
because it is fine to send one to another thread, because that thread now owns it and it no one else can access it. But it is not fine to share it because Cell
and RefCell
use interior mutability without synchronizing access. So if two different threads tried to access either of them there would be a data race.
Another case is if you are using trait objects.
Box<dyn Trait>: !Send + !Sync
Box<dyn Trait + Send>: Send + !Sync
Box<dyn Trait + Send + Sync>: Send + Sync
Box<dyn Trait + Sync>: !Send + Sync
The reason for this is because if you don't mark the trait object Send
or Sync
you can put types inside that are not Send
or Sync
.
It looks like DirectoryLock
is only Send
because of the Box<Drop + Send + 'static>
inside of it (from looking at the source code).
This basically means that as soon as you can send a reference to an object to another thread, the object must be able to handle access from both threads concurrently.
Another point of view is this:
Send
: an object can have its move ||
closure) for example: thread local things like a thread handle or a thread-dependent pseudo random number generatorSync
means that multiple threads can String
can be read by multiple threads immutably safely). Note that in rust, "sharing" an object, implicitly means that it can only be read, so an &T
, and not a &mut T
or smart pointers/guards, etc.Not immutable, but shared , because Mutex
and RwLock
are not immutable, but they are Send
.
(this gets at the idea that &T
means a shared borrow of T
, while &mut T
is a unique borrow of T
, and T
is an owned value)
The following impl
in ::core
,
unsafe impl<T : Send + ?Sized> Send for &'_ mut T {}
shows that
T : Send
expresses the fact that a unique handle to a T
(be it unique ownership (T
, Box<T>
, etc.) or a unique reference (&mut T
)) is safe to cross thread boundaries,
whereas T : Sync
expresses the fact that shared / aliased handles to a T
(be it shared ownership (mainly Arc<T>
) or a shared reference (&T
)) are safe to cross thread boundaries, thus exposing T
to potential parallel accesses. This requires either the predominant immutability of &T
(the reason why most stuff in Rust is Sync
) or, when mutation through shared references is possible (a.k.a. Interior Mutabilty), then such mutation must be guaranteed to be synchronised / data-race free (isn't the trait aptly named?).
Hence the counterexample given by @RustyYato: Cell<T : Copy>
and RefCell<T>
provide mutability of the wrappee T
just from shared references to the wrappers, and yet offer no synchronisation mechanism to guard against data races. That's why the wrappers cannot possibly be Sync
, and are thus !Sync
.
On the other hand, Mutex<T>
and RwLock<T>
, despite offering Interior Mutability, do provide a synchronisation mechanism to guard against data races (locks), thus remaining Sync
.
Something interesting to imagine is a non-Send
case that does not involve the trivial case (having a shared handle on a !Sync
thing):
#![feature(optin_builtin_traits)]
/// we want something that can be tested for equality and
/// where each instance is distinct from another
pub
unsafe trait IsUnique : Default + Eq {}
/// An implementation with a thread-local UID
#[derive(
Debug,
PartialEq, Eq,
)]
pub
struct ThreadUnique {
uid: u64,
}
impl Default for ThreadUnique {
fn default () -> Self
{
use ::core::cell::Cell;
thread_local! {
static UID: Cell<u64> = Cell::new(0);
}
let uid = UID.with(Cell::get);
UID .with(|slf| slf
.set(uid.checked_add(1).expect("UID overflow"))
);
Self { uid }
}
}
/// Ensure our thread local value cannot be sent to another thread
impl !Send for ThreadUnique {}
/// Ensure our thread local value cannot be read from another thread
impl !Sync for ThreadUnique {}
/// Now we *know* we are upholding the required invariants
unsafe impl IsUnique for ThreadUnique {}
#[test]
fn main ()
{
let x = ThreadUnique::default();
let y = ThreadUnique::default();
assert_ne!(x, y);
}
Now another interesting question, could something be !Send + Sync
? Maybe if it was immovable.
Not sure if it's possible to construct such an object using just safe rust and std
(except from obvious ones like Box<Trait + Sync>
), but one can imagine something like the following:
mod rcish {
pub struct Rcish<T> {
// note: this is private
rc: Rc<T>,
}
impl Rcish<T: Sync> {
pub fn new(x: T) -> Self { Self { rc: Rc::new(x) } }
pub fn this_is_threadsafe(&self) -> &T { &self.rc }
pub fn this_is_not(&mut self) -> &Rc<T> { &self.rc }
}
unsafe impl<T> Sync for Rcish<T> where T: Sync;
// no Send impl, because Rc is not Send
}
As we allow only access to the inner T
using &self
methods, this struct can be sync. But it's not send because we could obtain a reference to the Rc
and moreover, even destruction of such an object is unsafe.
Edit: I think there even was a crate implementing something similar to the example above, but unfortunately, I couldn't find it now.
Another case could be with an item using some thread_local
on Drop
but not when read:
Cool, I hadn't thought of that! Also I didn't know we had a compile_fail
doc test, that will come in handy when testing macros.
This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.