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 ::::>> {
let = 8;
::::(
// 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
TisSyncif and only if&TisSend.
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 ::::>> {
let = 8;
::::(|| {
.(|| !(&));
});
(())
}
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
SendorSyncby 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 containedTand 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 beSendto 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
SyncifT: SyncThis wasn't always the case! There used to be a bug where
MutexGuard<T>wasSyncifTisSend, sinceSyncis an auto-traitMutexGuard<T>only contains a&Mutex<T>and apoison::Guard, not aTdirectly&Mutex<T>: SyncifMutex<T>: Sync(which requiresT: Send), andpoison::Guard: Sync
The danger with having
MutexGuard<T>beSyncifT: Sendis the potential for data races ifTwere a type with interior mutability likeCell<U>, which isSend(ifU: Send) but!Sync.!SendA 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 beSyncwhenTis!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_mut(&mut self), returning&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
TisOption<U>whereUis!Send(implyingTis!Send),Option::take(&mut self)can returnSome(U), thereby sendingUfrom Thread A to Thread B.Therefore,
Mutex<T>cannot beSyncifTis!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>: SendT: Sendis required because movingArc<T>to another thread movesTalong with it.T: Syncis required becauseArc<T>::deref(&self)returns a&T.
For
Arc<T>: SyncT: Sendis required because you canArc<T>::clone(&self), then keep the clonedArcaround until it is the last strong reference then takeTout withtry_unwrapor dropTby dropping the clonedArc.T: Syncis 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!