Hedronite Lesson · Polyglot-Dev / Rust · Tue 2026-09-29

Shared state, Send/Sync

Lock before you touch. Arc clones the handle. Send and Sync name who may cross the thread boundary.

Lesson Class: Duha (Rust language track · Beat A · topic T1)
Focus: Mutex::lock / MutexGuard Drop · Arc vs Rc · Send / Sync marker traits
Code Blocks: clean blocks from TRPL 16-12..16-15, explanation in prose
Done-criteria: Can lock a Mutex<T> and say why Rc<T> is neither Send nor Sync
Grounding: TRPL stable ch16-03 + ch16-04 · Topics #20 (async) held
The Lock Gate
Mutex::lock returns MutexGuard; Drop unlocks. You cannot forget to release.
The Arc Bridge
Rc<Mutex<T>> fails Send; Arc::clone is the thread-safe share.
The Marker Fence
Send moves ownership across threads; Sync allows shared reference; Rc is neither.
Lock before you touch. Arc clones the handle. Send and Sync name who may cross the thread boundary.

<!-- hal:authoritative:yaml -->

Lock before you touch. Arc clones the handle. Send and Sync name who may cross the thread boundary.

§I - Frame

Duha session 19. Topics #19, TRPL Chapter 16: shared-state concurrency, then the Send and Sync marker traits. Session 18 taught spawn, join, and mpsc channels: share by communicating. This row is the other half of the chapter. Multiple threads touch one location, and the type system still holds the line.

Channels feel like single ownership: once you send, you no longer use the value. Shared memory feels like multiple ownership from Chapter 15. The book walks that parallel so you reuse what you already know about Rc and interior mutability, then watch the compiler refuse the single-thread versions at the thread boundary.

Three moves land by the end:

  1. The Lock Gate: Mutex::lock returns a MutexGuard; Drop unlocks. You cannot forget to release.
  2. The Arc Bridge: Rc<Mutex<T>> fails Send; Arc::clone is the thread-safe share.
  3. The Marker Fence: Send moves ownership across threads; Sync allows shared reference; Rc<T> is neither.

Done-criteria: Can lock a Mutex<T> and say why Rc<T> is neither Send nor Sync.

Async/await stays for Topics #20. Do not invent a Tokio runtime here.

§II - The Lock Gate

A mutex is mutual exclusion: one thread holds exclusive access at a time. You acquire the lock before you use the data. When you are done, you unlock so another thread can enter. Rust makes the second rule automatic.

use std::sync::Mutex;

fn main() {
    let m = Mutex::new(5);

    {
        let mut num = m.lock().unwrap();
        *num = 6;
    }

    println!("m = {m:?}");
}

Listing 16-12 starts single-threaded so the API is visible without a race. Mutex::new wraps the value. lock blocks until this thread owns the lock, then returns a MutexGuard inside a LockResult. unwrap panics if the lock is poisoned (another thread panicked while holding it). The guard implements Deref so *num reaches the i32, and its Drop releases the lock when the inner scope ends. You do not call unlock by hand. Forget the lock call and the type system blocks you: m is Mutex<i32>, not i32.

Thus the Lock Gate: acquire, mutate through the guard, let scope end.

§III - Sharing the mutex: move fails, Rc fails, Arc works

Ten threads each increment a counter. The naive move of Mutex into every spawn fails with E0382: the first iteration already moved counter. Wrap it in Rc the way Chapter 15 taught multiple ownership, and the error changes:

use std::rc::Rc;
use std::sync::Mutex;
use std::thread;

fn main() {
    let counter = Rc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Rc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Listing 16-14 does not compile. The important line: Rc<Mutex<i32>> cannot be sent between threads safely, because Send is not implemented for Rc<Mutex<i32>>. Rc updates its count without atomics. Two threads cloning or dropping at once can corrupt the count. That is why Rc stays single-threaded.

Arc is the atomic twin. Same API shape, thread-safe counts:

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let counter = Arc::new(Mutex::new(0));
    let mut handles = vec![];

    for _ in 0..10 {
        let counter = Arc::clone(&counter);
        let handle = thread::spawn(move || {
            let mut num = counter.lock().unwrap();
            *num += 1;
        });
        handles.push(handle);
    }

    for handle in handles {
        handle.join().unwrap();
    }

    println!("Result: {}", *counter.lock().unwrap());
}

Listing 16-15 prints Result: 10. Each spawn takes an Arc clone; lock serializes the increments; join waits; the final lock reads the total. Mutex here is also interior mutability across threads, the concurrent cousin of RefCell inside Rc. Deadlocks remain a logic risk the borrow checker does not catch. For a lone integer, std::sync::atomic can be simpler; the book uses Mutex so the lock story stays in view.

Thus the Arc Bridge: clone the Arc, move the clone, lock inside the thread.

§IV - The Marker Fence: Send and Sync

Almost every concurrency tool in this chapter lives in the standard library. Two marker traits live in the language: Send and Sync.

Send means ownership of the value may transfer to another thread. Almost every Rust type is Send. Rc<T> is not: cloning across threads would race the count. That is the Listing 16-14 error named as a trait bound. Arc<T> implements Send, so the counter program compiles. Types built only from Send parts are Send automatically. Raw pointers are the notable primitive exception (Chapter 20).

Sync means it is safe for the type to be referenced from multiple threads: T is Sync if &T is Send. Rc<T> is not Sync for the same count race. RefCell<T> and the Cell family are not Sync: their runtime borrow checks are not thread-safe. Mutex<T> is Sync, which is why shared access through an Arc works.

You do not implement these traits by hand for ordinary structs. Composition inherits them. Manual implementation is unsafe and belongs with the Nomicon later. For today, the fence is a reading rule: if thread::spawn rejects a type, ask which of Send or Sync is missing, and whether you reached for a single-thread smart pointer at a multi-thread door.

§V - Proof and close

  1. Lock a Mutex<i32>, mutate through the guard, and name what Drop does when the guard leaves scope.
  2. Reproduce the E0382 move of Mutex into a loop of spawns; then the E0277 from Rc::clone into spawn.
  3. Fix with Arc::clone and print a counter that reaches 10 after ten joins.
  4. State in one sentence why Rc<T> is neither Send nor Sync, and which trait Mutex<T> adds for shared access.

Done-criteria: Can lock a Mutex<T> and say why Rc<T> is neither Send nor Sync.

Next: async/await (Topics #20, TRPL Ch.17). Keep the Lock Gate and Marker Fence; #20 opens async fn / .await from the book without a Tokio acquire yet.

Related