Introduction
Take a look at this error:
error[E0277]: `YourType` cannot be sent between threads safely
--> src/main.rs:10:5
|
10 | thread::spawn(move || {
| ^^^^^^^^^^^^^ `YourType` cannot be sent between threads safely
|
= help: within `YourType`, the trait `Send` is not implemented for `SomeInnerType`
= note: required because it appears within the type `YourType`
= note: required because it is used within this closure
= note: required because it is used as the type of argument 1
If you have ever come across this type of error and you are wondering what Send
and Sync
are, you are at the right place. In this article, we will explain what Send
and Sync
are in simple terms. By the end of this article, you will understand what these traits are and how to avoid those gnarly errors.
What are Send and Sync?
In simple terms, they are traits. If you do not really understand traits, you can ensure you come back to this site sometime later—we will write on that topic. For now, just know that traits are a simple way to define shared behavior, sort of like interfaces or Rust's own version of polymorphism.
However, our initial definition is a little misleading. Send
and Sync
are not just any traits but marker traits. Marker traits are traits that do not have any function implementations and are only there to mark the status of their implementors. They are used at compile time to ensure certain standards are met. Some other examples are Sized
and Unpin
(stick around, we will be talking about Sized
very soon). Also, Send
and Sync
are auto traits, which means if all your internal types implement them, your type also implements them.
So they are auto marker traits that allow the compiler to know a couple of things during compile time:
Send
: If a type is safe to send into a thread, basically if you can pass ownership of the type into and out of a thread.Sync
: If a&T
of the type can be sent into a thread, basically if a shared reference of the type can be sent or shared between threads.
Primitive Types and Their Send
and Sync
Status
Since Send
and Sync
are auto traits, primitive types in Rust generally implement both traits. However, there are exceptions. Here's a breakdown:
Types That Are Both Send
and Sync
All numeric types:
i8
,i16
,i32
,i64
,i128
,u8
,u16
,u32
,u64
,u128
,f32
,f64
bool
char
()
, the unit typeArrays and tuples, if their elements are
Send
andSync
Types That Are Only Send
(but not Sync
)
Some raw pointers can be
Send
depending on their usage, but they require careful handling
Types That Are Neither Send
Nor Sync
Rc<T>
(Reference Counted pointer)UnsafeCell<T>
(used for interior mutability)Raw pointers
(*const T
,*mut T)
by default, unless explicitly made safe
If your custom type, including std::types like Cow
, collections, etc., is composed only of types that are Send
and Sync
, it will automatically implement both traits. Otherwise, you may need to adjust your design or use synchronization primitives like Arc<T>
or Mutex<T>
to ensure thread safety.
What is the Difference Between Send
and Sync
?
If you've asked yourself this question, do not worry—you are not alone. You just need to understand the difference between Send
and Sync
in Rust.
As explained earlier, a Send
type can be sent into a thread, while a Sync
type can be shared between threads. If you have both, then you can be sure that you can send and share the type between threads.
Let's go over this code sample. We will gradually work with this code until it works perfectly, and we hope you will understand these concepts better by the end.
First, create a project, call it whatever you want, then open the main.rs file.
fn main() {
println!("Let's get started");
}
That is what your main function would probably look like.
We will now create a scoped thread (something we will be talking about soon, so stick around). We are using scoped threads so you are not worried about lifetime errors. Basically, they will help us share non-static Send
+ Sync
variables in threads.
Go ahead and update your main.rs function to this:
use std::thread;
fn main() {
let value = String::from("value");
// create a thread scope
thread::scope(|s| {
// spawn thread one
s.spawn(|| {
dbg!(&value)
});
// spawn thread two
s.spawn(|| {
dbg!(&value);
});
});
}
Notice how we are able to reference the value in two different threads. This means this value is Sync
. To check that it is Send
, let us try moving it into one of the threads.
You can do this by updating your main.rs function to this:
use std::thread;
fn main() {
let value = String::from("value");
// create a thread scope
thread::scope(|s| {
// spawn thread one
s.spawn(move || {
dbg!(value)
});
// // spawn thread two
// s.spawn(|| {
// dbg!(&value);
// });
});
}
Now we are not referencing it anymore but taking an owned value, since the move
keyword forces variables used in a thread or closure to be owned by that scope. This compiles because this type is Send
, hence we can say this type is both Send
and Sync
.
It is worth noting that this would work for every Send
and Sync
type we mentioned earlier.
Note! Types that implement the
Copy
trait would simply be copied when amove
is called, hence this kind of code would still compile:
use std::thread;
fn main() {
let value = 0;
// create a thread scope
thread::scope(|s| {
// spawn thread one
s.spawn(move || {
dbg!(value)
});
// spawn thread two
s.spawn(|| {
dbg!(&value);
});
});
}
Now that you understand this, try tweaking that example by creating a struct and putting Send
and Sync
types in it to check if it is also Send
and Sync
.
You can share your observations.
What is Not Send
and Sync
and Why?
First, you should ask, why would anything not be Send
or Sync
? Well, these two traits revolve around thread safety. Since threads can run in parallel, actions with shared variables have to be atomic. In simple terms, if a type allows non-atomic mutable interaction, then it is not safe for Sync
(sharing across threads), since two threads can mistakenly run into a race condition or cause undefined behaviors. Similarly, a type won't be Send
if it contains a type that is not Send
or if it implements reference counting in a non-atomic way.
Raw pointers are neither safe for sending nor syncing unless explicitly marked as such, since you have to ensure their usage is atomic.
Let's show an example of some of our earlier described types that are unsafe for Send
and Sync
:
Update your main code to this:
use std::{cell::RefCell, thread};
fn main() {
let value = RefCell::new(0);
// create a thread scope
thread::scope(|s| {
// spawn thread one
s.spawn(|| {
dbg!(&value)
});
// spawn thread two
s.spawn(|| {
dbg!(&value);
});
});
}
All we did was change the String
in the initial example to a RefCell
, and we get this error:
`&RefCell<{integer}>` cannot be sent between threads safely
the trait `Sync` is not implemented for `RefCell<{integer}>`
required for `&RefCell<{integer}>` to implement `Send`
This proves our point. Since RefCells
allow non-atomic or basically unguarded mutability, we cannot be sure to allow two threads to share them since we could run into a race condition as both threads keep changing the variable, or even undefined behavior. To avoid this, we could use a type that allows internal mutability but in an atomic
way.
We could use Mutex
or RwLock
here—these two allow for mutable referencing atomically.
Update the code to this:
use std::{sync::Mutex, thread};
fn main() {
let value = Mutex::new(0);
// create a thread scope
thread::scope(|s| {
// spawn thread one
s.spawn(|| {
dbg!(&value)
});
// spawn thread two
s.spawn(|| {
dbg!(&value);
});
});
}
Now the code runs without any errors. But what if we wanted to have owned references of the values? We would need a reference-counted style to encapsulate them, so let's use the Rc
: this is a smart pointer that uses non-atomic reference counting to identify when a type has been borrowed and uses this count to identify when to drop the type.
Update code to this:
use std::{rc::Rc, sync::Mutex, thread};
fn main() {
let value = Rc::from(Mutex::new(0));
// create a ref counted clone of the value
let sec_value_ref = value.clone();
// create a thread scope and move the first clone into it
thread::scope(move |s| {
// spawn thread one
s.spawn(|| {
dbg!(&value)
});
// spawn thread two
s.spawn(|| {
dbg!(&value);
});
});
}
Immediately we get this error:
`&Rc<Mutex<{integer}>>` cannot be sent between threads safely
the trait `Sync` is not implemented for `Rc<Mutex<{integer}>>`
required for `&Rc<Mutex<{integer}>>` to implement `Send`
Notice that it says Send
and Sync
are not implemented for this type. This is because Rc
is neither Send
nor Sync
, which means this is not safe at all. To fix this, we would use the thread-safe version of Rc
that implements reference counting by using atomic operations, called Arc
:
use std::{sync::{Arc, Mutex}, thread};
fn main() {
let value = Arc::from(Mutex::new(0));
// create a ref counted clone of the value
let sec_value_ref = value.clone();
// create a thread scope and move the first clone into it
thread::scope(move |s| {
// spawn thread one
s.spawn(move || {
dbg!(value)
});
// spawn thread two
s.spawn(|| {
dbg!(sec_value_ref);
});
});
}
Glossary of Terms
Here's a quick reference for important terms used throughout this article:
Atomic operations: Operations that complete in a single step relative to other threads, without being interrupted. These operations are essential for thread safety.
Auto traits: Traits that are automatically implemented for a type if all of its component types also implement the trait.
Closure: An anonymous function that can capture values from its surrounding scope. Often used in Rust for passing operations to threads.
Interior mutability: A design pattern in Rust that allows you to mutate data even when there are immutable references to that data, through the use of special types like RefCell
and Mutex
.
Marker traits: Traits that don't define any methods but serve to "mark" types with certain properties. Send
and Sync
are prime examples.
Mutex: Short for "mutual exclusion." A synchronization primitive that prevents multiple threads from accessing shared data simultaneously.
Race condition: A bug that occurs when the timing or ordering of events affects a program's correctness, usually in multi-threaded contexts.
Reference counting: A technique for memory management where objects are only deleted when no references to them exist. Rc
and Arc
implement this in Rust.
Scoped threads: Threads that are guaranteed to complete before the scope ends, allowing them to safely borrow data from the parent scope.
Smart pointer: A data structure that acts like a pointer but also has additional metadata and capabilities, such as Rc
, Arc
, Box
, etc.
Thread safety: The property of code that can correctly handle multiple threads accessing it simultaneously without causing bugs.
Undefined behavior: Program behavior that is not specified by the language and could result in crashes, incorrect results, or security vulnerabilities.
Summary
TIP! A type is
Send
if it can move ownership into threads, whileSync
lets you share references between threads safely.
Now you understand the clear difference between both traits. To strengthen your knowledge, try writing a simple multi-threaded counter using different types.
For more information, check out:
Rust Atomics and Locks: This book would broaden your fundamental understand of atomics and why un-atomic types are not thread-safe.
The Rust Standard Library docs for
Send
andSync
: The official documentation detailing the guarantees and implementations of these traits.
So that is it , you now know how to handle that error . If you want more knowledge on using rust see our course.