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
-
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. -
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.
-
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. -
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 anUnsafeCell
). -
In those cases,
-
either the public non-
unsafe
&
-based APIs allow to perform mutation without synchronisation mechanisms, such asCell
orRefCell
, 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 withAtomicBool
,AtomicU8
,Atomic…
, as well asMutex
orRwLock
, we getSync
-ness back
-
-
Now, given a type
T
which is notSync
/ 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 asArc<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 notSend
/ safe to own across threads, thenT
cannot beSync
/ safe to share across threads).Thus:
T : Sync ⇔ &'_ T : Send
-
-
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
-
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 thatT : Sync
does not hold), then&'_ T : !Sync
and&'_ T : !Send
(and more generally, any copyable / cloneable type that yields shared access to the sameT
instance will be neitherSync
norSend
as well):T : !Sync
⇒ (&'_ T : !Sync
and&'_ T : !Send
) -
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 thatRc<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
andRefCell
. -
!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 ).
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 drop
ping 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).