We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Fearless Concurrency: Threads, Channels, Shared State step 20 of 24
Safe parallel mutation with chunks_mut
Scale the rows of a flat grid in place, in parallel.
pub fn parallel_scale_rows(
grid: Vec<i64>,
width: usize,
factors: Vec<i64>,
workers: usize,
) -> Vec<i64>
grid is rows * width elements in row-major order and factors has one
entry per row. Multiply every cell of row r by factors[r], distribute
bands of rows across workers scoped threads, and return the grid.
Empty grid or width == 0 returns the grid unchanged. Treat workers == 0
as 1. The tests check workers = 1, 3, 16 against the same expected grid.
Constraints, and they are the point: no unsafe, no Arc, no Mutex, no
atomics, no copying the grid. Threads mutate the same allocation directly
and the borrow checker proves it is sound.
Contrast this with the raw-pointer version
Earlier in this track you wrote scatter_unsafe: a SendPtr newtype, an
unsafe impl Send, a // SAFETY: comment, and a promise that your index
ranges were disjoint — a promise no tool could check, where a mistake was
undefined behaviour rather than a wrong answer.
Here you make the same guarantee, and the compiler checks it. That is not a small difference: it is the difference between a code review that has to reason about your arithmetic and one that does not.
How a safe API can hand out several &mut to one buffer
It looks like it should be impossible. Aliasing-xor-mutation says one &mut
at a time, and here are several, live at once, into the same Vec.
The resolution: they point at disjoint parts. &mut [i64] is not “a
mutable reference to the vector”, it is exclusive access to these elements.
Two exclusive accesses to non-overlapping regions do not alias, so nothing is
violated.
What the borrow checker cannot do is prove disjointness from arithmetic. Given
grid[i] and grid[j] it has no idea whether i != j. So std provides
splitting functions that are structurally disjoint, and each does the unsafe
reasoning once, internally, where it can be audited:
slice.split_at_mut(mid) // -> (&mut [T], &mut [T])
slice.chunks_mut(n) // -> iterator of non-overlapping &mut [T]
slice.iter_mut() // -> one &mut T per element, all distinct
chunks_mut(width) gives you exactly one &mut [i64] per row, and by
construction no two overlap. This is precisely how rayon‘s
par_chunks_mut is sound underneath. You are writing the real thing, just
with a manual thread pool.
The starter’s error
The starter indexes the grid from several closures:
error[E0499]: cannot borrow `*grid_ref` as mutable more than once at a time
| s.spawn(move || {
| ^^^^^^^ second mutable borrow occurs here
| grid_ref[r * width + c] *= f;
| -------- second borrow occurs due to use in closure
The strided access pattern is disjoint — worker w touches rows
w, w + workers, ... and no two workers share a row. The borrow checker
cannot see it, and it is right not to guess. Rejecting a correct program is
the price of never accepting a broken one.
Note the error is E0499, the plain “two mutable borrows” error you met long
before threads existed. thread::scope removed the 'static requirement; it
did not and could not remove aliasing rules.
The obstacle you have to get past
chunks_mut returns an iterator, and you cannot index an iterator or hand
slice k of it to thread k directly. That is deliberate — it is what stops
you accidentally holding two.
The way through, in one line: collect the row slices first, then chunk that.
let mut rows: Vec<&mut [i64]> = grid.chunks_mut(width).collect();
for band in rows.chunks_mut(band_size) {
s.spawn(move || { /* band: &mut [&mut [i64]] */ });
}
Vec<&mut [i64]> is a vector of exclusive row handles, and chunks_mut on
it hands out disjoint groups of rows. Two applications of the same idea.
Track which row index each band starts at, because the band no longer knows.
Once you have written it, the reason rayon exists is obvious — and so is the
fact that it is a convenience over this, not a different mechanism.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.