Skip to content

← Fearless Concurrency: Threads, Channels, Shared State step 8 of 24

Hard Framework

unsafe impl Send, and the disjoint-capture trap

Scatter a list of (index, value) writes into a zeroed buffer, in parallel, through a raw pointer.

pub fn scatter_unsafe(len: usize, writes: Vec<(usize, i64)>, workers: usize) -> Vec<i64>

Allocate vec![0i64; len], split 0..len into workers disjoint half-open ranges, and give every worker the same raw *mut i64. Each worker scans the whole writes list in order and applies only the writes whose index lands in its own range. Return the buffer.

Indices are always < len. Duplicate indices are allowed and the last write in input order wins — which is automatic if you keep the scan in order, because every index belongs to exactly one worker.

len == 0 returns []; treat workers == 0 as 1.

This problem can produce undefined behaviour, and that is the point

Everything else in this track is safe code where the compiler proves you right. Here you are making the promise instead. If your ranges overlap, two threads write the same i64 concurrently, and that is a data race — undefined behaviour, not “a wrong number”. The grader cannot detect it. It may pass. It may pass on your machine and fail on someone’s ARM laptop. This is the real cost of unsafe, and meeting it deliberately once is worth more than reading about it ten times.

Two problems from now you will write the same computation with chunks_mut, where the borrow checker proves disjointness for you and no unsafe appears at all. Compare them when you get there.

Why the newtype exists

*mut i64 is not Send. Auto traits are structural: a closure capturing a raw pointer is not Send, so spawn refuses it. That is the compiler saying “I have no idea whether this is safe”, which is correct — a raw pointer carries no information about who else can reach it.

The escape hatch is to make the promise yourself:

struct SendPtr(*mut i64);
unsafe impl Send for SendPtr {}

unsafe impl is a proof obligation, not an escape hatch. You are telling the compiler you have an argument it cannot see. Write that argument down — the reference solution carries a // SAFETY: comment naming both facts the proof rests on (disjoint ranges, all threads joined before the buffer is read). If you cannot write the comment, you do not have the proof.

The trap — and it will get you

The starter has the unsafe impl. It still fails:

error[E0277]: `*mut i64` cannot be sent between threads safely
   = help: within `{closure@...}`, the trait `Send` is not implemented for `*mut i64`

SendPtr is Send. So why is the closure complaining about *mut i64?

Because since edition 2021, closures capture individual fields, not whole variables (RFC 2229, “disjoint capture”). The closure body mentions p.0 and nothing else about p, so what it captures is p.0 — a bare *mut i64, which is not Send. Your unsafe impl applies to SendPtr, and no SendPtr was ever captured.

This change was made for good reasons — it lets two closures borrow different fields of one struct — and here it silently defeats a pattern that used to work. The fix is to make the closure name the whole value. The blunt version is one line at the top of the closure:

s.spawn(move || {
    let p = p;            // forces the whole SendPtr to be captured
    ...
});

The tidier version is to make the field private and hand out the pointer through a method — p.get() takes &self, so the whole struct is captured. Either works. Knowing why either works is the item.

Two lints that will not save you here

Worth knowing what the toolchain is and is not doing for you:

  • clippy::non_send_fields_in_send_ty would flag exactly this kind of unsafe impl. It is nursery, so it is off by default and will not fire under this gate.
  • clippy::undocumented_unsafe_blocks would force the // SAFETY: comments. It is restriction, also off by default.

Neither is a criticism of clippy. It is a calibration: on the unsafe side of the line, the tools have far less to say, and the discipline has to come from you.

Loading visualization…