This post is my personal notes for grokking
Send
andSync
in Rust. It's not formal, and will assume that you're basically familiar with concurrency and synchronisation, as well as Rust's main wrapper types. In particular, remember that Rust values are always owned by exactly one variable and taking references must satisfy aliasing xor mutability.
Here's the secret: you shouldn't be worrying about Send
and Sync
. They're the default. Almost everything is Send
and Sync
, and the compiler will auto-derive them for every type it can. The issue is !Send
and !Sync
, or really just: !Send
.
So what is !Send
?
A type is !Send
when values can't be owned on one thread and then moved to another. Because of single-ownership it couldn't be owned by two threads simultaneously, this is a restriction across the whole life of the value.
!Send
:= this value is locked to the thread that created it
That's the core concept behind both !Send
and !Sync
. I'll get to when this is the case later, but first let's talk references.
If we can have T: !Send
, we can also have &U: !Send
since T
could be &U
. This case is particularly interesting, since if we own a value of type T
we can create as many &T
values as we like.
This means that unless &T: !Send
, we can have as many &T
values on as many threads as we like. This is great for the most part: &T
is immutable so there are no data-races... unless T
contains interior mutability. Interior mutability exactly means being able to mutate T
behind a &T
reference. This sounds like a recipe for data races! In such cases we'll need &T: !Send
to prevent them. This is so important that it gets its own name...
Surprise !Sync
!
T: !Sync
simply means &T: !Send
. Interpreting a bit, !Sync
means that a value cannot be referenced by multiple threads at all.
!Sync
:= references to this value are locked to its thread
This almost means that !Send
implies !Sync
. After all, if a value cannot be used by more than one thread at different times, how could it possibly be allowed by more than one thread at the same time? This is often true, but not a logical requirement, because !Sync
is about whether shared references (&T
) can be used on multiple threads at the same time, not the value itself. It is possible (but fairly rare) for a type to be !Send
but still Sync
, for example if your type is backed by some thread-local resource but all behaviour visible through &T
does not depend on it.
So is this type !Send
or !Sync
?
There are a bunch of rules of thumb. But I think the key question they boil down to is: could this type be used to move a !Send
value (which may be a &T: !Send
) to another thread?
Rules of thumb for !Sync
:
- Your type transitively contains any
!Sync
type, unless wrapped by aMutex
or similar synchronisation primitive. - Your type contains interior mutability which is not synchronised. For example, it contains
Cell
orRefCell
. - If you can use a
&T
to take ownership of any!Send
type.- This is normally the case if your type is
!Send
itself.
- This is normally the case if your type is
- Your type contains raw pointers and you haven't manually proven and implemented
Sync
.
Rules of thumb for !Send
:
- Your type transitively contains any
!Send
type. - Your type is a handle to a resource which it owns non-uniquely, and access to that resource is not synchronised.
- For
&T
, this is exactly rule 2 for!Sync
, since ifT
has interior mutability that means that&T
is a shared-ownership handle toT
.
- For
- Your type contains raw pointers and you haven't manually proven and implemented
Send
.
Examples
-
Rc
-- This is a handle to a resource that is jointly owned, therefore!Send
since (for example)Rc::get_mut
is not synchronised. Moreover,Rc
has interior mutability for the reference count, which is unsynchronised, so!Sync
. -
Arc
- Avoids the problems ofRc
by synchronising the reference count and access appropriately using atomics, thus bothSend
andSync
. -
RefCell
-- Archetypal example of interior mutability with no synchronisation, therefore!Sync
, however since the wrapped value is unqiuely owned thenRefCell
isSend
when the wrapped value is. -
Mutex
-- If it contains a!Send
type then it's!Send + !Sync
since it provides full ownership of the contained type. Otherwise, it is bothSend
by unique ownership of aSend
, andSync
by enforcing synchronisation itself.
Raw pointers are interesting. Rust marks all raw pointer types as !Send
and !Sync
, but moving them (and their references) between threads isn't in-and-of-itself a problem. The problem comes when you try to use (that is, dereference) that pointer. That action is already marked as unsafe
, so Rust could have allowed them to be Send
and Sync
, but it is considered so easy to break Send
and Sync
with raw pointers that you need to additionally implement the corresponding unsafe
traits to mark your type as Send
or Sync
.
No free Send
wrapping
"I've got this annoying value, how do I just make the damn thing Send
and Sync
already!?" I hear you cry.
Bad news, I'm afriad.
The better news is that if the type is !Sync
but is Send
, then you can wrap it in a Mutex
or similar synchronisation type and that will make it both Send
and Sync
.
The very bad news is that !Send
types can only be made Send
by unsafe impl Send for T
-- which you should absolutely not do unless you very much know what you're doing.
Truly !Send
types (that is, basically anything !Send
except carefully used raw pointers) are stuck on their thread. This is the entire point of the feature, anything else and you're exposed to data races.
Your alternatives for dealing with !Send
types is to, for example:
- Serialise the data contained and send that to another thread where it can be re-constructed.
- Use channels or other inter-thread communication to indirectly "talk to" the
!Send
thread when needed.
When do I force !Send
or !Sync
?
It's possible that you're writing some struct that would make no sense to send to other threads (or send references to other threads), but the compiler cannot work this out itself. This is rare, since the compiler will generally work it out before you, but possible if the issue is one of higher-level correctness that the compiler cannot reason about.
For example, suppose you're wrapping some library behind FFI and you know (from the library docs) that the resource you're working with is thread-local. However, the "handle" that library gives you to said resource is just a bare primitive, like u32
. Rust has no idea that u32
is !Send
(acting more like a pointer) until you tell it.
Right now, it's not terribly easy to force !Send
or !Sync
, since negative impls are only available on nightly. The work around is to use a PhantomData
of some type that already has the !Send
or !Sync
you require, so that gets inherited. For example, winit::EventLoop
contains a PhantomData<*mut ()>
explicitly for this purpose.
When do I force Send
or Sync
?
It is, of course, possible to manually implement Send
and Sync
on something the compiler has decided is !Send
and !Sync
. This is how std
collections (as well as others) which work on raw pointers implement Send
and Sync
appropriately.
This power -- like any use of unsafe
-- should absolutely not be taken lightly. Read the nomicon, reason carefully, and write good tests. Don't just unsafe impl Send
because you're frustrated, that way lies Undefined Behaviour and Madness.
Top comments (4)
Hey, really nice post, I got a lot clearer understanding of the subject now.
Just a little doubt, to be sure I got it right: in the example about Rc, shouldn't it say "This is a handle to a resource that is jointly owned, therefore !Send since (for example) Rc::get_mut is not synchronised. Moreover, Rc has interior mutability for the reference count, which is unsynchronised, so !Sync." (i.e. swap Sync and Send in the sentence)?
Because as I understand it, being a handle to a non-uniquely owned resource whose access is not synchronised is rule #2 for being !Send, and having (unsynchronised) interior mutability is rule #2 for being !Sync, hence the first part of the sentence should be used to prove it's !Send and the second part to prove it's !Sync.
Good catch, thank you. I'll fix the text.
Sorry for the late reply, I assumed I had email alerts set up but apparently I don't.
Same question.
Thanks @jmaargh. This post explains very well one of the most confused concept in Rust.