We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 8 of 24
unsafe impl Send, and the disjoint-capture trap
Scatter a list of (index, value) writes into a zeroed buffer, using several
threads that all write through the same raw pointer.
pub fn scatter_unsafe(len: usize, writes: Vec<(usize, i64)>, workers: usize) -> Vec<i64>
The contract
- Start from
vec![0i64; len]. Every position that no write touches stays0. - Every index in
writesis less thanlen. Values can be anyi64, including negative ones. - Several writes may target the same index. The last one in input order
wins. For example, with
len = 4and writes[(1, 10), (1, 20), (2, 3)]the result is[0, 20, 3, 0]. len == 0returns an empty vector (andwritesis then empty).- Treat
workers == 0as 1.workerscan be larger thanlen, up tousize::MAX. Do not spawn a thread for an empty range; with a very largeworkersvalue that would mean trying to spawn an enormous number of threads. - Return the buffer after all threads have finished.
The intended approach
Split 0..len into disjoint half-open ranges (each start..end
includes start and excludes end), one per worker. A
balanced split works well: with base = len / workers and
extra = len % workers, the first extra ranges have base + 1 indices and
the rest have base. Any split into disjoint ranges that together cover
0..len gives the same output.
Give every worker a copy of the same *mut i64 (the address of the buffer's
first element). Each worker scans the whole writes list in input order and
performs only the writes whose index falls in its own range. Because each
index belongs to exactly one worker, and that worker sees the writes in input
order, the last write to an index is the one that remains.
Why this exercise uses unsafe
Writing through a raw pointer requires an unsafe block, so the compiler
does not check that the threads stay out of each other's way. You have to
make sure of that yourself. If two ranges overlapped, two threads could write
the same i64 at the same time without synchronization. That is a data
race, and a data race is undefined behavior: Rust makes no promise about
what the program does, not merely a promise of "some wrong number". Ordinary
tests cannot reliably detect it. A racy program may produce the expected
output on every run here and still be incorrect.
The same computation can be written without unsafe by splitting the buffer
with chunks_mut, which hands each thread its own &mut [i64] and lets the
borrow checker prove the pieces are disjoint. This exercise shows what you
take on when you give up that proof.
Why a wrapper type is needed
thread::Scope::spawn (like thread::spawn) requires the closure to be
Send, meaning it is safe to move to another thread. A closure is Send
only if what it captures is suitable for that: for a move closure, every
captured value must be Send. Raw pointers (*mut T and *const T) are
never Send, because the type says nothing about who else can reach the
memory they point to. A closure that captures a *mut i64 is therefore
rejected.
The usual workaround is a wrapper type with a manual implementation:
struct SendPtr(*mut i64);
unsafe impl Send for SendPtr {}
unsafe impl means "I promise this is sound, for reasons the compiler cannot
check". Write the reasons down in a // SAFETY: comment. The official
solution's comment names the two facts the promise depends on: the ranges are
disjoint, and thread::scope joins every worker before the buffer is read or
returned, so no thread writes after the buffer's memory is freed. If you
cannot state facts like these, the unsafe impl is not justified.
Why the starter still fails
The starter already contains SendPtr and its unsafe impl Send, yet
compiling it under edition 2024 fails with an error that never mentions
SendPtr:
error[E0277]: `*mut i64` cannot be sent between threads safely
= help: within `{closure@...}`, the trait `Send` is not implemented for `*mut i64`
The reason is how closures capture variables. Since edition 2021, a closure
captures the specific places it uses (a variable, or a field path inside
one), not necessarily the whole variable. This rule comes from RFC 2229,
"disjoint closure captures". The closure body uses only p.0, so the move
closure captures the field p.0, which is a bare *mut i64. No SendPtr
value is captured, so the unsafe impl Send for SendPtr never applies.
Under edition 2018, closures captured whole variables, and the same starter
compiles. The newer rule lets closures use different fields of one struct
independently, but it changes which types matter for Send checks in cases
like this one.
The fix is to make the closure use the whole SendPtr. One way is a single
statement at the start of the closure body:
s.spawn(move || {
let p = p; // uses all of `p`, so the closure captures the whole SendPtr
// ... the loop, writing through `p.0` ...
});
Another way is a method such as fn get(&self) -> *mut i64 { self.0 } and
writing through p.get() instead of p.0. Calling a method on p uses the
variable p itself, so the move closure captures the whole SendPtr.
Making the field private (by putting SendPtr in its own module) additionally
stops code outside that module from reaching .0 directly, but privacy is not
what fixes the capture. Either fix works.
What Clippy does and does not check here
The grading gate runs Clippy's default lints. Two optional lints are related to this code, and neither replaces your own reasoning:
clippy::non_send_fields_in_send_ty(in the off-by-defaultnurserygroup) looks forunsafe impl Sendon types with fields that are notSend, such as anRc. It deliberately allows raw-pointer fields, so it reports nothing forSendPtr, even when enabled.clippy::undocumented_unsafe_blocks(in the off-by-defaultrestrictiongroup) reportsunsafeblocks andunsafe impls that lack a// SAFETY:comment. It checks that a comment exists, not that the argument in it is correct.
Your code, saved here
Sign in to access your accepted solutions and saved draft history. You can keep reading the lesson without an account.
Sign in to your accountStuck?
Rust reference solution
Sign in to track your attempts toward unlocking the reference solution.