I saw a question on Reddit, that asks: “Why does T
need to be Send
for a Mutex
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 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
- Say, you have a string.
- And you want to share this string with multiple threads.
- 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:
- We have an
Rc<String>
variable. - We clone it multiple times.
- At this point, we cannot get a mutable reference to our String via
Rc<String>
clones, because there are multipleRc<String>
pointers present. - 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 viaRc<String>
.
Multi-threaded example:
- We have an
Rc<String>
variable. - We clone it multiple times.
- 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).
- 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. - 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 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 Send
ing 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>>
:
- We created our
Mutex<Cell<String>>
object. - We shared
&Mutex<Cell<String>>
(references to our original Mutex object) with multiple threads. Mutex
logic guarantees that only one thread can access ourCell<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 Send
ing 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 docs 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 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.
- We created our
Mutex<Rc<String>>
object. - We shared
&Mutex<Rc<String>>
(references to our original Mutex object) with multiple threads. Mutex
logic guarantees that only one thread can access ourRc<String>
object at a time.- Yet, these threads can still clone the
Rc<String>
, and these clones will be valid even after they drop theMutexGuard
(got out of the critical region). - And now, we have multiple threads having access to
Rc<String>
clones, outside of Mutex scope. - These threads can drop the
Rc<String>
clones simultaneously, and since reference counting forRc
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. HenceT
needs to beSend
in the first place.
I have a counter argument to this.
- We are NOT
Send
ingT
to other threads inMutex<T>
scenario - We are sending references to
T
(I think it is fair to countMutexGuard
as a reference toT
). Mutex<T>
does NOT require&T
to beSend
, onlyT
to beSend
.
Additionally, notice that there is no Sync
bound on T
in order for Mutex<T>
to be Sync
. Small reminder:
T
isSync
if and only if&T
isSend
.
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 Send
ing 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
.