Send without Sync is possible?

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
https://docs.rs/tantivy/0.9.1/tantivy/directory/struct.DirectoryLock.html

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).

5 Likes

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 ownership exclusive access transferred from one thread to another (for example with a move || closure) for example: thread local things like a thread handle or a thread-dependent pseudo random number generator
  • Sync means that multiple threads can immutably interact with share an object at the same time (for example a 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)

1 Like

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 @KrishnaSannasi: 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):

Click to expand
#![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);
}
4 Likes

Now another interesting question, could something be !Send + Sync? Maybe if it was immovable.

1 Like

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.

1 Like

Another case could be with an item using some thread_local on Drop but not when read:

2 Likes

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.

2 Likes