Example of a type that is not `Send`?

For completeness's sake, I think it may be interesting to know of types which are Sync (safe to share across threads) but which are nevertheless not Send (safe to own across threads): Rc is neither, so the difference is harder to grasp.

Some reminders

  1. By sharing I mean &-access, be it directly –&T references–, or indirectly (capable of ultimatetly yielding & access): Arc<T>, &Arc<T>, &&T, &&mut T, etc.

  2. The main cause of "unsafety to share across threads", or, more generally, the main reason not to be "thread-safe", are data races: multiple threads potentially accessing (read or write) the same data at the same time with at least one of them performing a write / performing mutation.

  3. But since we are talking of shared access, and, by default, shared access is an immutable access, most Rust types are automagically Sync / safe to share across threads, since shared access prevents mutation in their cases.

  4. So, the main culprit not to be Sync, is when we lose the shared-thus-immutable causality. That is, when Shared Mutability / Interior Mutability is involved (in technical Rust-specific terms, when, ultimately, data ends up wrapped in an UnsafeCell ).

  5. In those cases,

    • either the public non-unsafe &-based APIs allow to perform mutation without synchronisation mechanisms, such as Cell or RefCell, and in that case we are indeed very much not-Sync;

    • or all the public non-unsafe &-based APIs that could perform mutation do guard against data races through synchronization primitives. In such cases, such as with AtomicBool, AtomicU8, Atomic…, as well as Mutex or RwLock, we get Sync-ness back :slight_smile:

  6. Now, given a type T which is not Sync / not safe to share across threads, consider a type such as &T, or any other type able to:

    • when owned, yield a &T;

    • be "copyable" or cloneable;

    The two main examples of this are the type &T itself, as well as Arc<T> (or &Arc<T>, etc.).

    Such a type is thus unsafe to own (send in an owned or unique fashion) across threads, since we can copy / clone it and then send the copy/clone across a thread. Now, we have the original handle, and the sent one, which refer to the same entity (it is thus shared), from across thread boundaries. Given they were not Sync, this is unsafe, and thus, the "sending the clone" operation was unsound.

    From all this, we get that such types are not Send (and, trivially, if &T is not Send / safe to own across threads, then T cannot be Sync / safe to share across threads).

    Thus:

    T : Sync ⇔ &'_ T : Send


  1. Another one which this time is obvious, is that since sharing is transitive (if you share a shared reference to something, then you are ultimately sharing that thing: from a &&T one can get a &T, and vice versa).

    Thus:

    T : Sync ⇔ &'_ T : Sync

  2. This means that there is an obvious pattern that we encounter a lot across Rust types with shared mutability, which is that if T : !Sync (abuse of notation I'll be using here to say that T : Sync does not hold), then &'_ T : !Sync and &'_ T : !Send (and more generally, any copyable / cloneable type that yields shared access to the same T instance will be neither Sync nor Send as well):

    T : !Sync ⇒ (&'_ T : !Sync and &'_ T : !Send)

  3. And it turns out that Rc<T> is, among other things, a &'reference_counted Cell<usize>, that is, a shareable handle to an unsynchronized mutable counter.

    Since Cell<usize> : !Sync, it follows that Rc<T> : !Send + !Sync.

More generally, at this point, you will observe that the moment we lose Sync somewhere, we lose both Sync and Send at the next level of indirection, hence why we mostly encounter:

  • Send + !Sync types, which are the "original !Sync type". The two default examples are the ones I mentioned before regarding unsynchronized shared mutability: Cell and RefCell.

  • !Send + !Sync, which are the types that manipulate shared handles to !Sync types. Rc<T> is such an example.

The challenge of finding !Send + Sync types

At this point, it should become clearer more obvious that such types are hard to come by. Such a combination means the type is:

  • safe to share across threads,

  • unsafe to own / have unique access to across threads.

Do such creatures exist? What do they look like?

And the answer is yes: the mechanism to fit those constraints, is for the type to feature &mut-based (or, equivalently, owned-based) APIs that are generally not thread-safe. If only the &mut-based and owned-based APIs are not thread safe, and all the non-unsafe &-based APIs are innocuous, we then get a non-Send and yet Sync type!

Artificial example: thread-local singleton types

#![feature(negative_impls)]
mod privacy_boundary { // paramount when talking about safety and APIs
    use ::core::cell::Cell;

    pub
    struct Singleton {
        _private: (),
    }

    thread_local! {
        static EXISTS: Cell<bool> = false.into();
    }

    impl Singleton {
        pub
        fn new ()
          -> Option<Self>
        {
            if EXISTS.with(|it| it.replace(true)) {
                None
            } else {
                Some(Singleton { _private: () })
            }
        }
        
        pub
        fn unreachable (
            self: &'_ mut Singleton,
            _: &'_ mut Singleton,
        ) -> !
        {
            unsafe {
                // Safety: thanks to `Singleton : !Send`, this
                // situation is unreachable
                ::std::hint::unreachable_unchecked()
            }
        }
    }

    impl Drop for Singleton { // `&mut`-based / owned-based API.
        fn drop (self: &'_ mut Singleton)
        {
            EXISTS.with(|it| it.set(false));
        }
    }

    impl !Send for Singleton {}
    /* // The following already holds because of the fields of `Singleton`
    impl Sync for Singleton {} */
}

In this example, we have created a type, Singleton, that verifies that, within a given thread, there is at most one instance of it. With this, the Singleton::unreachable can use the dangerous optimization of using unreachable_unchecked(), since for it to be reached, we'd need to have two unique references (&mut) to a Singleton within the same thread (the one performing the Singleton::unreachable() call). And since you cannot have multiple unique references to the same entity, it means we'd be dealing with two different Singleton instance uniquely accessible from within the same thread.

Since this is guarded at construction time for each thread, the only way for it to happen would be for unique access to the Singleton to be able to cross the thread boundary, i.e., we'd need Singleton : Send to violate the invariants and cause unsoundness with Singleton::unreachable().

Hence Singleton : !Send, and yet we can perfectly have Singleton : Sync, since the &Singleton-based APIs do nothing harmful (in this example, they actually do nothing at all :grinning_face_with_smiling_eyes:).

A real-world example: MutexGuard

For a more realistic example, it turns out that MutexGuard is not Send, since for the definition of MutexGuard to be maximally portable, it needs to support the POSIX API of pthreads, which states that a lock can only be released from within the thread where it was created.

Since releasing an acquired lock, in Rust parlance, is about dropping a MutexGuard, it means that a MutexGuard cannot be dropped in a different thread (from the one where it was created). This means owned instances of MutexGuard cannot cross thread boundaries, i.e., MutexGuard<'_, _> : !Send. But we do have MutexGuard<'_, T> : Sync where T : Sync, since besides that lock-release oddity, a MutexGuard<'_, T> behaves like a &'_ mut T (it is a handle through which one is guaranteed unique / exclusive access to the T for (almost all of) its lifetime '_; hence its DerefMut<Target = T> implementation), and as explained before, &'_ mut T : Sync where T : Sync (the only thing one can do with a &&mut T is reborrow a &T out of it).

34 Likes