← Debugging Guides

阅读中文版本

Rust — error[E0277]: closure cannot be sent between threads safely

Fix `std::thread::spawn` requires closure to be Send errors by finding the Rc, RefCell, or raw pointer captured across the boundary, and swapping in thread-safe types.

intermediatedebuggingrustconcurrencytraits

Symptoms

  • error[E0277]: `Rc<String>` cannot be sent between threads safely inside a spawn call
  • The note says required because it is used within the closure and points at the spawn requirement Send
  • The code compiles single-threaded but fails the moment work moves to thread::spawn or a thread pool

Possible causes

  • The closure captures an Rc or Rc<RefCell<...>> shared handle, which is not Send by design
  • A raw pointer or non-thread-safe struct (some FFI wrappers) is captured into the closure
  • A global lazily-initialized value uses thread-unsafe initialization captured by the closure
  • Moving work into a pool (rayon, threadpool) after writing it with single-threaded assumptions

Diagnosis steps

Read the type chain in the error

cargo check 2>&1 | grep -B3 -A12 E0277

Interpretation

The diagnostic names the exact non-Send type and the capture that carries it — the fix list writes itself from that message.

Find the capture sites

grep -Rn "Rc::new\|Rc<\|RefCell" src/ | head -20

Interpretation

Every Rc captured by a thread closure is a Send violation; these lines are the migration surface.

Confirm the replacement compiles

cargo check && cargo test

Interpretation

Arc and Mutex versions satisfy Send and Sync; green build plus tests proves the swap kept semantics.

Root cause

std::thread::spawn requires the closure and its captures to be Send, because the closure crosses a thread boundary by value. Rc uses non-atomic reference counting, so sending it to another thread could race the counter; the compiler therefore rejects the spawn at compile time rather than allowing a data race.

Fix

Replace Rc with Arc for cross-thread sharing, wrap interior mutability with Mutex or RwLock instead of RefCell, keep single-threaded Rc types out of closures destined for spawn, and move owned data into the closure rather than sharing where possible.

Risk note

Blanket Arc<Mutex<...>> wrapping everything degrades to lock contention. Prefer moving ownership or using message passing with channels for coordination.

Verify the fix

The threaded version compiles and its tests pass; a stress test exercising the shared state across threads with no deadlocks or panics.

Symptom walkthrough

The error names the smuggler and the border:

error[E0277]: `Rc<String>` cannot be sent between threads safely
 --> src/main.rs:12:19
  |
12 |     thread::spawn(move || {
  |     ------------- ^-- within the closure, `Rc<String>`
  |     |             cannot be sent between threads safely
  = note: required because it appears within the closure
  = note: required by `std::thread::spawn`

The type chain in the note is the audit list. Single-threaded code compiles Rc freely — the failure appears exactly when the closure crosses spawn, which is why the error seems to “arrive” with a refactor to threads or a pool.

Fix in practice

The type swap, by sharing style:

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

// shared mutable state across threads:
let counter = Arc::new(Mutex::new(0usize));
let handles: Vec<_> = (0..4).map(|_| {
    let counter = Arc::clone(&counter);
    thread::spawn(move || {
        *counter.lock().unwrap() += 1; // Send + Sync satisfied
    })
}).collect();
for h in handles { h.join().unwrap(); }

Read-only sharing needs only Arc:

let config = Arc::new(load_config()); // no Mutex: immutable share

And often the best fix is not sharing at all — move owned data in, get results back through a channel:

let (tx, rx) = mpsc::channel();
thread::spawn(move || {
    tx.send(expensive(chunk)).unwrap(); // ownership moved in, result out
});

Verification

cargo check passes with no E0277 and no Rc captures remaining in spawn-bound closures; re-run the grep for Rc and RefCell inside threaded modules to keep the audit list empty. The migrated tests pass single-threaded and threaded alike — run the suite through the pool implementation to prove semantics survived the type swap. Add a stress test that hammers the shared Arc Mutex state across threads for a bounded duration and asserts the final value equals the expected count with no deadlocks or panics. Distinguish the related flavor: E0277 for Sync fails on sharing a reference across threads, while the Send flavor fails on moving the value in — the note’s type chain names which boundary you crossed.