C.W.K.
Stream
Lesson 05 of 05 · published

Send, Sync & Fearless Concurrency

~11 min · concurrency, send, sync, fearless

Level 0Rust Curious
0 XP0/80 lessons0/19 achievements
0/100 XP to next level100 XP to go0% complete

How can the compiler promise no data races? Through two marker traits — Send and Sync — that encode thread-safety into the type system. They're the foundation everything in this track stands on.

Send: safe to move between threads

A type is Send if it's safe to transfer ownership of it to another thread. Most types are. The notable exception is Rc — its non-atomic count would race if two threads cloned it at once, so Rc is not Send, and the compiler refuses to move it into a thread. Arc (atomic) is Send, which is why it's the cross-thread choice.

Sync: safe to share by reference

A type is Sync if &T is safe to share between threads — multiple threads can hold references to it at once. Mutex<T> is Sync (its locking makes concurrent access safe); a plain RefCell is not (its runtime borrow tracking isn't atomic). The compiler checks these automatically.

Send and Sync are how 'fearless' is enforced. They're auto-derived marker traits the compiler reasons about: it lets you spawn a thread or share a reference only when the types involved prove they're race-safe. Get it wrong and it won't compile. The guarantee isn't discipline — it's the type system.

You rarely write them, but they're always working

You almost never implement Send/Sync by hand — they're automatically derived for types built from other Send/Sync types. But they're why every example in this track compiles only when it's actually safe. When a concurrency mistake won't compile and the error mentions Send or Sync, that's the type system catching a data race before it could ever run.

Code

Arc is Send; Rc is not — the compiler enforces it·rust
use std::rc::Rc;
use std::sync::Arc;
use std::thread;

fn main() {
    // Arc is Send: this compiles and is race-free
    let shared = Arc::new(5);
    let a = Arc::clone(&shared);
    thread::spawn(move || println!("arc: {a}")).join().unwrap();

    // Rc is NOT Send. The following would NOT compile:
    //   let r = Rc::new(5);
    //   let r2 = Rc::clone(&r);
    //   thread::spawn(move || println!("{r2}"));
    //   error: `Rc<i32>` cannot be sent between threads safely
    let _local = Rc::new(0); // Rc is perfectly fine WITHIN one thread
    println!("{_local}");
}

External links

Exercise

Share a read-only value across threads with Arc and confirm it compiles. Then swap in Rc and read the Send error in full. In your own words, what data race is the Send bound preventing, and why does Arc satisfy it while Rc doesn't?
Hint
Rc's reference count is a plain integer; two threads incrementing it simultaneously would race and corrupt it. Arc uses atomic increments that can't race. Send encodes exactly this distinction, so the compiler only lets Arc cross threads.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.