This isn’t a thorough tutorial of the Send
and Sync
traits, rather my notes to help me understand it as best I can.
See the Rust book, the Rustonomicon for more details, or check the sources list at the bottom for more interesting articles.
This also means that there may be inaccuracies or downright falsehoods here!
None intentional, but please let me know if there are any.
Send
A type is Send
if it is safe to send it to another thread.
“Sending” means transferring ownership, or moving the variable to the new thread.
// This program prints:
// [src/main.rs:6] x = 8
fn () -> <(), <dyn std::error::>> {
let = 8;
std::thread::(
// The `move` keyword transfers ownership of `x`
move || !(),
)
.()
.();
(())
}
Most types are Send
. Additionally, any type that only consists of Send
types are Send
as well.
Sync
T
isSync
if and only if&T
isSend
.
A type is Sync
if it is safe to share it with another thread.
“Sharing” means allowing multiple threads to reference the same variable, which can only happen if a reference to that variable can be sent to the other thread.
// This program prints:
// [src/main.rs:4] &x = 8
fn () -> <(), <dyn std::error::>> {
let = 8;
std::thread::(|| {
.(|| !(&));
});
(())
}
In the above example, since x
is Sync
, &x
is Send
, and we can reference x
from another thread.
Always Not Send and Not Sync
Here are a couple types that are !Send
and !Sync
by default:
-
Raw pointers (
*const T
,*mut T
)These are not
Send
orSync
by default because it is up to the programmer to ensure proper usage of the pointers, as they are commonly used in FFI. Unlike references, the compiler does not track the lifetime of pointers. -
Reference-counting pointers (
Rc<T>
)This type allows a single thread to hold multiple
Rc<T>
to a sharedT
. The containedT
and any reference counts are stored in a shared memory location to allow shared access from multipleRc<T>
.Since the shared memory location is accessed without synchronization,
Rc<T>
cannot beSend
to prevent another thread from causing a data race.Similarly,
Rc<T>
cannot beSync
, because cloning anRc<T>
can be done with a shared reference, which implies accessing the underlying reference count.
Sometimes Send but Not Sync
The Cell<T>
type is Send
if T
is Send
.
It is !Sync
because it allows interior mutability, which allows retrieving, setting, or swapping T
given a &Cell<T>
.
This access is unsynchronized, which can cause a data race if done concurrently from multiple threads.
For similar reasons, the RefCell<T>
type is Send
if T: Send
but !Sync
.
Sometimes Sync but Not Send
The MutexGuard<T>
type is
-
Sync
ifT: Sync
This wasn’t always the case! There used to be a bug where
MutexGuard<T>
wasSync
ifT
isSend
, since-
Sync
is an auto-trait -
MutexGuard<T>
only contains a&Mutex<T>
and apoison::Guard
, not aT
directly -
&Mutex<T>: Sync
ifMutex<T>: Sync
(which requiresT: Send
), andpoison::Guard: Sync
The danger with having
MutexGuard<T>
beSync
ifT: Send
is the potential for data races ifT
were a type with interior mutability likeCell<U>
, which isSend
(ifU: Send
) but!Sync
. -
-
!Send
A mutex must always be unlocked on the same thread that locked it. Since the mutex is unlocked by dropping its guard, the guard cannot be sent to another thread.
Sometimes Send or Sync
Some wrapper types are Send
or Sync
if and only if their inner type are Send
or Sync
respectively.
An example of that is Option<T>
.
However, some types have notably different constraints.
The Mutex<T>
type is Send
if T
is Send
, but Sync
if T
is Send
.
This was slightly confusing at first. Here’s a hopefully lucid proof by counterexample:
-
Assume
Mutex<T>
can beSync
whenT
is!Send
. -
Since
Mutex<T>
isSync
, Thread A can share&Mutex<T>
with Thread B. -
Thread B can then call
Mutex::lock(&self)
, returning aMutexGuard<T>
. -
Thread B can then call
MutexGuard::deref_
, returningmut(&mut self) &mut T
. So far so good, the mutex is still used as it normally is. -
We can take ownership through mutable references. For example, if
T
isOption<U>
whereU
is!Send
(implyingT
is!Send
),Option::take(&mut self)
can returnSome(U)
, thereby sendingU
from Thread A to Thread B. -
Therefore,
Mutex<T>
cannot beSync
ifT
is!Send
.
Why does T
not have to be Sync
then?
It is because getting access to &mut T
(hence &T
or T
) requires locking the mutex, which ensures that only one thread access at a time!
That’s the whole point of a Mutex, to allow sharing unshareable types.
The Arc<T>
type is also a bit confusing.
It is only Send
or Sync
if T: Send + Sync
.
-
For
Arc<T>: Send
-
T: Send
is required because movingArc<T>
to another thread movesT
along with it.T: Sync
is required becauseArc<T>::deref(&self)
returns a&T
.
-
-
For
Arc<T>: Sync
-
T: Send
is required because you canArc<T>::clone(&self)
, then keep the clonedArc
around until it is the last strong reference then takeT
out withtry_
or dropunwrap T
by dropping the clonedArc
.T: Sync
is required becauseArc<T>::deref(&self)
returns a&T
.
-
Exclusive References
Mutable references &mut T: Send
require T: Send
.
This has a straightforward reason: it’s easy to transfer ownership of a T to another thread if you have mutable access, for example with Option::take(&mut self)
.
Additionally, &mut T: Sync
if and only if T: Sync
.
While this might seem like you can share mutable references across threads, in actuality it means that you can send shared references to mutable references (& &mut T
), which act as immutable references.
Conclusion
Writing this helped me understand Send
and Sync
more, so I consider that a win!