Send & Mutex

I saw a question on Reddit, that asks: “why T needs to be Send in order to Mutex<T> to be Sync?”. I think that’s a great question, and in here I’ll try my best to shine some light on that.

Send & Sync (intro)

These are two marker traits (or auto-traits) implemented automatically by the compiler, that are very important for coding in Rust. They indicate that a type is safe to use (access) between threads.

Here are the definitions I like better than official documentation (Jonathan Giddy’s answer on stackoverflow):

  • Sync: allows an object to to be used (accessed) by two threads A and B at the same time. This is trivial for non-mutable objects, but mutations need to be synchronized (performed in sequence with the same order being seen by all threads). This is often done using a Mutex or RwLock which allows one thread to proceed while others must wait. By enforcing a shared order of changes, these types can turn a non-Sync object into a Sync object. Another mechanism for making objects Sync is to use atomic types, which are essentially Sync primitives.
  • Send: allows an object to be used (accessed) by two threads A and B at different times. Thread A can create and use an object, then send it to thread B, so thread B can use the object while thread A cannot. The Rust ownership model can be used to enforce this non-overlapping use. Hence the ownership model is an important part of Rust’s Send thread safety, and may be the reason that Send is less intuitive than Sync when comparing with other languages.

Important: I specifically did not use the definition:

  • A type is Send if it is safe to send it to another thread.

I think it is misleading, and we will see why.

If the definitions I copy-pasted from stackoverflow were confusing, let me explain by examples. I usually like to start with Sync.


Sync

  1. Say, you have a string.
  2. And you want to share this string with multiple threads.
  3. If more than one thread tries to modify the same string at the same time, you will get a data race.

This is very much like the borrow checker of Rust, ensuring there is only one writer at a time, but there can be multiple readers.

Our astute readers should immediately think, can multiple threads read the string at the same time? Because, this should be possible.

And yes, it is possible! You can share as many immutable references of this string &str as you like with multiple threads. The Rust borrow checker already forbids a mutable reference to exist at the same time as any other reference to the same object.

So, String is Sync. And nearly everything you encounter in Rust is Sync.

Then, what is not Sync? The below is from the official documentation:

“Types that are not Sync are those that have “interior mutability” in a non-thread-safe form, such as Cell and RefCell. These types allow for mutation of their contents even through an immutable, shared reference. For example the set method on Cell<T> takes &self, so it requires only a shared reference &Cell<T>. The method performs no synchronization, thus Cell cannot be Sync.”

In other words, there are some specific types in Rust, that allow you to modify the content even through an immutable reference. And if there is no synchronization mechanism embedded in those types (such as Cell and RefCell), they are not Sync.

This should make sense, because imagine you share many immutable references to your Cell<String> with different threads. Since Cell<T> allows you to mutate the content, and there is no synchronization mechanism present, you have a potential data race!


Send

Again, nearly everything in Rust is Send. Let’s start with what is not Send? I’ll use the infamous Rc<T> example:

(reminder: Rc is the reference counted pointer type)

Single-threaded example:

  1. We have an Rc<String> variable.
  2. We clone it multiple times.
  3. At this point, we cannot get a mutable reference to our String via Rc<String> clones, because there are multiple Rc<String> pointers present.
  4. If we drop some Rc<String> clones, so that there is only one reference remaining, then we can get a mutable reference to our String via Rc<String>.

Multi-threaded example:

  1. We have an Rc<String> variable.
  2. We clone it multiple times.
  3. We send these clones to different threads (actually we can’t do that, because Rust compiler forbids us to, but let’s say we can for the sake of this example).
  4. At this point, we cannot get a mutable reference to our String, because there are multiple Rc<String> pointers present. So, let’s drop some of them.
  5. If two threads tries to drop their Rc<String> clones at the same time, a data race on the reference counting mechanism will occur. And we will never be able to get a mutable reference to our String.

#ggwp #ez


Deep Dive

Mutex

We saw that it is possible to share immutable references across threads for nearly everything (with few exceptions). But what about sharing mutable references? Rust’s compiler should allow us to modify something across threads in a safe way.

Enters Mutex and RwLock. They are both synchronization mechanisms that allows us to modify our data, only if no one else at that time is neither reading nor writing to it (locking).

Thus, although Mutex grants interior mutability across threads, it is still Sync, thanks to its locking mechanism.

The same applies to RwLock.

Finally, we can start to answer our question: “Why T needs to be Send in order to Mutex<T> to be Sync?”


Sync and Send relationship

To answer our question, we have to take a quick detour:

T is Sync if and only if &T is Send.

If it’s your first time encountering this, you may start to question your life. It’s understandable.

But it’s very simple in fact. Remember the definition of Sync: allows an object to to be used by two threads A and B at the same time.

How can an object can be used by more than 1 thread at the same time? By sending its references to those threads. So, definition of Sync can be also interpreted as: safe to share the immutable references of the object with other threads.

This means, we are Sending references of our object to other threads, which implies the references of our object should be safe to send across threads. Now, read again:

T is Sync if and only if &T is Send.


Sync and Mutex

This is the last step for our detour.

Does T needs to be Sync in order to Mutex<T> to be Sync?

If you want a good mental challenge, don’t read below.

To answer this, let’s start with something that is not Sync and see what happens. We already saw Cell for interior mutability, which does not have any synchronization mechanism, so it is not Sync.

Cell and Mutex

We cannot send &Cell<String> across threads, because Cell allows us to mutate the inner part through immutable references. And multiple modifications at the same time means data race.

It is safe to send Cell<String> across threads though. Why? Because Cell has the ownership of the String, and if you send Cell<String> to another thread, you will be transferring this ownership to that thread. In other words, there still will be a single Cell<String> object, and it will be safe to mutate it.

Now, let’s think about Mutex<Cell<String>>:

  1. We created our Mutex<Cell<String>> object.
  2. We shared &Mutex<Cell<String>> (references to our original Mutex object) with multiple threads.
  3. Mutex logic guarantees that only one thread can access our Cell<String> object at a time.

Recall the problem with Cell was: having multiple references to our object, which can potentially mutate them simultaneously.

If Mutex enforces only one access at most at a time, then the problem with Cell disappears. This means our Mutex<Cell<String>> is Sync, although Cell<String> is not Sync.

In conclusion: Even though T is not Sync, Mutex<T> is still Sync. It should be straight-forward when you think: Sync is related to simultaneous access from different threads. If Mutex limits this to one access at a time, the internal data does not have to be Sync.


Send and Mutex

Here is where things get confusing. If we send &Mutex<T> across threads, does it mean we are Sending T?

And the answer is: No, We are not sending T. The best way to inspect that would be to analyze the function calls that we need to call for mutating T. Here is the proof from official docs:

pub fn lock(&self) -> LockResult<MutexGuard<'_, T>>

Means, when you lock the &Mutex<T>, you get a MutexGuard

And again from official docs on MutexGuard:

The data protected by the mutex can be accessed through this guard via its Deref and DerefMut implementations.

And again from official dosc on DerefMut:

Used for mutable dereferencing operations, like in *v = 1;.

If we are not sending T, then why T needs to be Send for Mutex<T> to be Sync?

This confusion is due to namings. I urge you to re-read the definition I gave for Send above, in fact, here it is again:

  • Send: allows an object to be used by two threads A and B at different times. Thread A can create and use an object, then send it to thread B, so thread B can use the object while thread A cannot. The Rust ownership model can be used to enforce this non-overlapping use. Hence the ownership model is an important part of Rust’s Send thread safety, and may be the reason that Send is less intuitive than Sync when comparing with other languages.

Notice that, the broader concept is: to be used by two threads A and B at different times. To be able to move the object across threads safely is only one way to grant access to the object from different threads at different times. One of the other ways is to use a Mutex as we just saw.

If we take the ubiquitous definition of Send, for example in in The Rustonomicon, the definition is made like this: A type is Send if it is safe to send it to another thread., it won’t be obvious to us what is the relationship between T being Send and Mutex<T> being Sync? Because we are not even sending T to another thread.

I believe the confusion arises from the name Send. It suggests it has only to do with the object being sent across threads. However, the main focus is on to be used by two threads A and B at different times. Hence, I specifically chose that definition instead.

Example: Mutex<Rc>

Keeping the definition above in mind, let’s analyze Mutex<Rc<String>>. We know that Rc<String> is not Send (due to Rc is not Send), so let’s see where the problem arise, and how it matches with the definition above.

  1. We created our Mutex<Rc<String>> object.
  2. We shared &Mutex<Rc<String>> (references to our original Mutex object) with multiple threads.
  3. Mutex logic guarantees that only one thread can access our Rc<String> object at a time.
  4. Yet, these threads can still clone the Rc<String>, and these clones will be valid even after they drop the MutexGuard (got out of the critical region).
  5. And now, we have multiple threads having access to Rc<String> clones, outside of Mutex scope.
  6. These threads can drop the Rc<String> clones simultaneously, and since reference counting for Rc is not done in an atomic way, you get a data race.

#ggwp #ez #pz #lemonsqueezy


Conclusion

For the case Mutex<Rc<String>>: we did not send Rc<String> to another thread at all. We just allowed multiple threads to use it at different times. And that was enough to have a data race.

The whole scenario makes perfect sense with the definition of Send: safe to be used by two threads A and B at different times.

So the definition I preferred over the official one, does not neglect the original one, but just encapsulates it.

If you think about safe to move the object to different threads, this is just one way to ensure it will be safe to be used by two threads A and B at different times, because we are passing the ownership and invalidating the object in the previous thread.

Hope this article made the definitions of Send and Sync clearer for you. Happy coding!

Bonus: Potential Pitfalls

One may want to stick with the official definition of Send, and try to explain why T needs to be Send in Mutex<T> with the following argument:

  • We are basically sending the reference of T to other threads. Hence T needs to be Send in the first place.

I have a counter argument to this.

  • We are NOT Sending T to other threads in Mutex<T> scenario
  • We are sending references to T (I think it is fair to count MutexGuard as a reference to T).
  • Mutex<T> does NOT require &T to be Send, only T to be Send.

Additionally, notice that there is no Sync bound on T in order to Mutex<T> to be Sync. Small reminder:

  • T is Sync if and only if &T is Send.

This means, from another point of view, &T does not have to be Send either.

In plain English, Mutex<T> does not care about the behavior of &T. It only cares about the behavior of T.

Mutex<T> example is the sole reason I find the definition: safe to move the object to different threads unsatisfying. Since we are not Sending the T to other threads in the: Mutex<T> example, but references to T, and yet we do not require to put any bound on &T.