DEV Community

jmaargh
jmaargh

Posted on • Edited on

Rust's `Send` and `Sync`, but actually the opposite

This post is my personal notes for grokking Send and Sync 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:

  1. Your type transitively contains any !Sync type, unless wrapped by a Mutex or similar synchronisation primitive.
  2. Your type contains interior mutability which is not synchronised. For example, it contains Cell or RefCell.
  3. If you can use a &T to take ownership of any !Send type.
    • This is normally the case if your type is !Send itself.
  4. Your type contains raw pointers and you haven't manually proven and implemented Sync.

Rules of thumb for !Send:

  1. Your type transitively contains any !Send type.
  2. 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 if T has interior mutability that means that &T is a shared-ownership handle to T.
  3. 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 of Rc by synchronising the reference count and access appropriately using atomics, thus both Send and Sync.
  • RefCell -- Archetypal example of interior mutability with no synchronisation, therefore !Sync, however since the wrapped value is unqiuely owned then RefCell is Send 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 both Send by unique ownership of a Send, and Sync 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:

  1. Serialise the data contained and send that to another thread where it can be re-constructed.
  2. 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)

Collapse
 
abigagli profile image
Andrea Bigagli

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.

Collapse
 
jmaargh profile image
jmaargh

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.

Collapse
 
yevgnen profile image
Yevgnen

Same question.

Collapse
 
quackquack profile image
Quack Quack

Thanks @jmaargh. This post explains very well one of the most confused concept in Rust.