Skip to content

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

Medium Framework

Safe parallel mutation with chunks_mut

Multiply each row of a grid by its own factor, using scoped threads to update disjoint bands of rows.

pub fn parallel_scale_rows(
    grid: Vec<i64>,
    width: usize,
    factors: Vec<i64>,
    workers: usize,
) -> Vec<i64>

The grid is stored row by row in one vector. For example, with width = 3, [1, 2, 3, 4, 5, 6] contains two rows. With factors [2, 10], return [2, 4, 6, 40, 50, 60].

Requirements

  • Return an empty grid or a grid with width == 0 unchanged, without reading the factors.
  • Otherwise, the input contains complete rows: grid.len() is a multiple of width, and factors.len() equals the number of rows.
  • Multiply cell (r, c) by factors[r]. All products fit in i64.
  • Treat zero workers as one, and cap the worker count at the number of rows. Split the rows into contiguous bands and spawn at most that many scoped threads. The result must be independent of the worker count.
  • Mutate the original grid allocation. Do not copy the grid or use unsafe, Arc, locks or atomics. Allocating a vector of borrowed row handles is fine.

The output tests check the returned values. They do not by themselves prove that an implementation uses threads or follows the allocation constraints; those are part of the implementation exercise.

Split the borrow before spawning

Each worker needs exclusive access to its own elements, not to the whole vector. Safe slice APIs can provide several mutable references at once when their regions do not overlap:

  • split_at_mut(mid) divides a slice into two parts at a valid boundary.
  • chunks_mut(n) yields non-overlapping chunks, with a possibly shorter final chunk. The chunk size must be positive.
  • iter_mut() yields a mutable reference to each element.

You can keep several chunks alive at the same time. For example:

fn main() {
    let mut values = [1, 2, 3, 4];
    let mut chunks = values.chunks_mut(2);
    let first = chunks.next().unwrap();
    let second = chunks.next().unwrap();
    first[0] = 10;
    second[0] = 30;
    assert_eq!(values, [10, 2, 30, 4]);
}

Splitting encodes the non-overlap guarantee in the references you receive. You still need to choose the right boundaries and pair each row with the right factor. Memory safety does not establish those application rules.

Why the starter fails

The starter creates one &mut Vec<i64> named grid_ref, then tries to move it into a new closure on every loop iteration. Rust reports E0382, a use of a moved value: mutable references are not Copy, so the first closure takes that reference and later closures cannot take it again.

The intended strided row indices do not give the closures separate borrows. Give each closure an already disjoint slice instead. thread::scope allows the threads to borrow local data because it joins them before returning; it does not relax the rules for exclusive access.

Here is a complete example with two halves:

fn main() {
    let mut values = [1, 2, 3, 4];
    let (left, right) = values.split_at_mut(2);
    std::thread::scope(|scope| {
        scope.spawn(move || {
            for value in left {
                *value *= 2;
            }
        });
        scope.spawn(move || {
            for value in right {
                *value *= 10;
            }
        });
    });
    assert_eq!(values, [2, 4, 30, 40]);
}

Build bands of rows

One approach is to collect grid.chunks_mut(width) into a Vec<&mut [i64]>. This allocates row handles, not copies of the cells. Apply chunks_mut(band_size) to that vector to borrow disjoint groups of handles, then move each group into a scoped thread.

For a nonempty grid, first clamp the worker count as described above. Set band_size = row_count.div_ceil(worker_count). This avoids the potentially overflowing addition in (row_count + worker_count - 1) / worker_count. Ten rows and six workers give bands of at most two rows, so this approach starts five threads, which is within the requested limit.

Record each band's starting row before moving it into its closure. A row at offset i within that band uses factors[start + i]. Workers can share an immutable borrow of the factors while mutating separate grid rows.

Collecting handles is one option, not a requirement of chunks_mut. You can also split the flat grid directly at row-aligned boundaries. In either design, the scope creates fresh threads and waits for them; it is not a reusable thread pool. For small grids, thread setup can cost more than the multiplication, so this exercise teaches safe partitioning rather than promising a speedup.