Send, Sync and thread safety
Every thread safety error in this chapter comes from two marker traits. Send says a value may be moved to another thread, and Sync says a shared reference to it may be used from another thread. Neither has a method; the compiler derives them for most types and refuses the ones it cannot prove.
This lesson moves a value that is Send, shares one that is Sync, and shows both refusals: an Rc that may not travel and an Arc<Cell> whose contents may not be shared. The last section writes an unsafe impl Send by hand.
Send: a value moves
A Cell<i32> cannot be shared between threads, but it can be moved to one, because moving gives the new thread the only copy. The compiler knows that and allows the spawn.
The program moves a cell into a worker, reads it there and returns the incremented value. No reference crosses the boundary, so only Send is required.
use std::cell::Cell;
use std::thread;
fn main() {
let counter = Cell::new(5);
let worker = thread::spawn(move || counter.get() + 1);
println!("{}", worker.join().unwrap());
}
rustc --edition 2024 cell_moved.rs
./cell_moved
6
The closure takes the cell by move, so the main thread no longer has it, and the interior mutability inside the worker is ordinary single-threaded code. The lesson on RefCell uses the same cell without a thread.
Sync: a value is shared
Arc<T> is Send only when T is Send and Sync, because every thread that holds a handle can take a shared reference to the value. A String is both, so two threads can read the same text.
The program shares a string between two threads and adds the lengths they report, which is the size twice.
use std::sync::Arc;
use std::thread;
fn main() {
let text = Arc::new(String::from("shared"));
let mut handles = Vec::new();
for _ in 0..2 {
let text = Arc::clone(&text);
handles.push(thread::spawn(move || text.len()));
}
let total: usize = handles.into_iter().map(|h| h.join().unwrap()).sum();
println!("{total}");
}
rustc --edition 2024 shared.rs
./shared
12
Six bytes twice is twelve, and the two reads happen at the same time without a lock because a shared reference to a String cannot change it. The lesson on Arc is the reference count behind the handle.
Not Send: Rc
Rc updates its count without atomics, so two threads could corrupt it. The type is therefore not Send, and a spawn that moves one is refused with E0277 before any code runs.
The program moves an Rc<i32> into a thread. The compiler reports the missing trait on the closure.
use std::rc::Rc;
use std::thread;
fn main() {
let value = Rc::new(1);
thread::spawn(move || {
println!("{value}");
});
}
rustc --edition 2024 rc_err.rs
error[E0277]: `Rc<i32>` cannot be sent between threads safely
--> rc_err.rs:6:19
|
6 | thread::spawn(move || {
| ------------- ^------
| | |
| _____|_____________within this `{closure@rc_err.rs:6:19: 6:26}`
| | |
| | required by a bound introduced by this call
7 | | println!("{value}");
8 | | });
| |_____^ `Rc<i32>` cannot be sent between threads safely
|
= help: within `{closure@rc_err.rs:6:19: 6:26}`, the trait `Send` is not implemented for `Rc<i32>`
note: required because it's used within this closure
--> rc_err.rs:6:19
|
6 | thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/thread/functions.rs:125:0
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The fix is Arc, which is the same type with an atomic count. The error is the same one a C++ program would produce only with a static analyser, which is the point of the two traits. The lesson on threads shows the spawn itself.
Not Sync: Cell
A Cell passes the first test and fails the second: it can move, but a shared reference to it lets any thread write it without a lock. Arc<Cell<i32>> therefore needs Cell to be Sync and is refused.
The program shares a cell through an Arc and writes it in a worker. The compiler names the value and the missing trait, and the note suggests the two replacements.
use std::cell::Cell;
use std::sync::Arc;
use std::thread;
fn main() {
let counter = Arc::new(Cell::new(0));
let shared = Arc::clone(&counter);
thread::spawn(move || {
shared.set(1);
});
}
rustc --edition 2024 arc_cell.rs
error[E0277]: `Cell<i32>` cannot be shared between threads safely
--> arc_cell.rs:8:19
|
8 | thread::spawn(move || {
| _____-------------_^
| | |
| | required by a bound introduced by this call
9 | | shared.set(1);
10 | | });
| |_____^ `Cell<i32>` cannot be shared between threads safely
|
= help: within `Cell<i32>`, the trait `Sync` is not implemented for `Cell<i32>`
= note: if you want to do aliasing and mutation between multiple threads, use `std::sync::RwLock` or `std::sync::atomic::AtomicI32` instead
= note: required for `Arc<Cell<i32>>` to implement `Send`
note: required because it’s used within this closure
--> arc_cell.rs:8:19
|
8 | thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/31fca3adb283cc9dfd56b49cdee9a96eb9c96ffd/library/std/src/thread/functions.rs:125:0
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0277`.
The third note is the reasoning in one line: Arc<T> is Send only when T is Sync, because every thread with a handle can borrow the value. The suggested replacements are the two types of the atomics lesson and the mutex lesson.
An unsafe impl Send
A type with a raw pointer is neither Send nor Sync, because the compiler cannot know what the pointer points at. The programmer can promise Send with an unsafe impl and a comment that states why the promise holds.
The wrapper below holds a pointer and claims Send. The program moves a null handle to a worker and asks it whether the pointer is null, which needs no dereference.
struct Handle(*mut u8);
// SAFETY: the pointer is either null or owned by the thread that receives the value.
unsafe impl Send for Handle {}
impl Handle {
fn is_null(&self) -> bool {
self.0.is_null()
}
}
fn main() {
let handle = Handle(std::ptr::null_mut());
let worker = std::thread::spawn(move || handle.is_null());
println!("{}", worker.join().unwrap());
}
rustc --edition 2024 handle.rs
./handle
true
The method call is what makes the closure capture the whole wrapper instead of the pointer field, and the whole wrapper is Send because of the implementation. An unsafe impl is a promise of the same weight as an unsafe block, and the comment beside it is not optional in code that another person maintains. The next lesson removes the need for most of these hand-written bounds.
Exercises
- Wrap a Vec<i32> in an Arc and share it with three threads; explain why it is allowed.
- Try to move a RefCell<i32> into a thread and read the error.
- Write a type with a raw pointer and decide whether it should be Send, Sync or neither.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does
Sendmean for a type?Answer
A value of it may be moved to another thread
Send is about moving a value across a thread boundary.
-
What does
Syncmean for a type?Answer
A shared reference to it may be used from another thread
Sync is about sharing a reference, not moving the value.
-
Why is
RcnotSend?Answer
Its count is updated without atomics
Two threads would race on the count, so the type is refused at the spawn.
-
Why is
Arc<Cell<i32>>notSend?Answer
The value inside is not Sync, so the Arc is not Send
Every thread with a handle can borrow the cell, so the cell has to be Sync.
-
What does this program print?
use std::thread; fn main() { let text = String::from("abc"); let worker = thread::spawn(move || text.len()); println!("{}", worker.join().unwrap()); }Answer
3
A String is Send, so the move into the thread is allowed and the length is three.
-
What does this program print?
use std::sync::Arc; use std::thread; fn main() { let value = Arc::new(7); let copy = Arc::clone(&value); let worker = thread::spawn(move || *copy + 1); println!("{} {}", worker.join().unwrap(), Arc::strong_count(&value)); }Answer
8 1
The worker reads the shared number and the handle is dropped when it ends.
-
What does this program print?
use std::cell::Cell; fn main() { let counter = Cell::new(2); let worker = std::thread::spawn(move || counter.get() * 3); println!("{}", worker.join().unwrap()); }Answer
6
A Cell is Send, and only Send is needed for a value that moves without a reference.
-
What does this program print?
use std::sync::Arc; use std::thread; fn main() { let value = Arc::new(String::from("ok")); let a = Arc::clone(&value); let b = Arc::clone(&value); let first = thread::spawn(move || a.clone()); let second = thread::spawn(move || b.clone()); let mut names = vec![first.join().unwrap(), second.join().unwrap()]; names.sort(); println!("{names:?}"); }Answer
["ok", "ok"]
Both threads clone the shared string and return their own copy.
-
What is the first line of the compiler output for this program?
use std::rc::Rc; use std::thread; fn main() { let value = Rc::new(1); thread::spawn(move || { println!("{value}"); }); }Answer
error[E0277]:
Rc<i32>cannot be sent between threads safelyThe closure moves the Rc, and the Rc does not implement Send.
-
What is the first line of the compiler output for this program?
use std::cell::Cell; use std::sync::Arc; use std::thread; fn main() { let counter = Arc::new(Cell::new(0)); let shared = Arc::clone(&counter); thread::spawn(move || { shared.set(1); }); }Answer
error[E0277]:
Cell<i32>cannot be shared between threads safelyThe Arc is Send only if the value is Sync, and the cell is not.
The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev