Let’s walk through the process of turning this into parallel code (with minimal changes from your original code, and using manual threads and the standard library instead of rayon
’s thread pool) step by step.
The first step is to set up a thread::scope
and spawn a thread for every loop iteration that ought to happen in parallel
fn extremas(x: &[u64], chunks: &Vec<(usize, usize)>, k: usize) -> (u64, u64) {
let mut minr = vec![0_u64; k];
let mut maxr = vec![0_u64; k];
thread::scope(|s| {
for i in 0..k {// <- Loop to do in parallel
s.spawn(|| {
let mut a = x[0];
let mut b = x[0];
for j in (chunks[i].0)..=(chunks[i].1) {
if x[j] > b {
b = x[j];
} else if x[j] < a {
a = x[j];
}
}
minr[i] = a;
maxr[i] = b;
});
}
});
min_max(&minr, &maxr)
}
This results in errors including these ones:
error[E0499]: cannot borrow `minr` as mutable more than once at a time
--> src/main.rs:28:21
|
26 | thread::scope(|s| {
| - has type `&'1 Scope<'1, '_>`
27 | for i in 0..k {// <- Loop to do in parallel
28 | s.spawn(|| {
| - ^^ `minr` was mutably borrowed here in the previous iteration of the loop
| _____________|
| |
29 | | let mut a = x[0];
30 | | let mut b = x[0];
31 | | for j in (chunks[i].0)..=(chunks[i].1) {
... |
38 | | minr[i] = a;
| | ---- borrows occur due to use of `minr` in closure
39 | | maxr[i] = b;
40 | | });
| |______________- argument requires that `minr` is borrowed for `'1`
error[E0499]: cannot borrow `maxr` as mutable more than once at a time
--> src/main.rs:28:21
|
26 | thread::scope(|s| {
| - has type `&'1 Scope<'1, '_>`
27 | for i in 0..k {// <- Loop to do in parallel
28 | s.spawn(|| {
| - ^^ `maxr` was mutably borrowed here in the previous iteration of the loop
| _____________|
| |
29 | | let mut a = x[0];
30 | | let mut b = x[0];
31 | | for j in (chunks[i].0)..=(chunks[i].1) {
... |
39 | | maxr[i] = b;
| | ---- borrows occur due to use of `maxr` in closure
40 | | });
| |______________- argument requires that `maxr` is borrowed for `'1`
The problem is that minr
and maxr
are accessed mutably from multiple threads now, we need to split them up in a way that the compiler understands that what we’re doing is fine (since each thread only views their own entry in those Vec
s. We can do that as in my previous post where a loop was parallelized by using an iterator instead of indexing, and using iter::zip
to iterate multiple things in lock-step, just as for i in 0..k { … minr[i] … maxr[i] … }
does.
fn extremas(x: &[u64], chunks: &Vec<(usize, usize)>, k: usize) -> (u64, u64) {
let mut minr = vec![0_u64; k];
let mut maxr = vec![0_u64; k];
thread::scope(|s| {
for (minr_i, maxr_i) in std::iter::zip(&mut minr, &mut maxr) {
s.spawn(|| {
let mut a = x[0];
let mut b = x[0];
for j in (chunks[i].0)..=(chunks[i].1) {
if x[j] > b {
b = x[j];
} else if x[j] < a {
a = x[j];
}
}
*minr_i = a;
*maxr_i = b;
});
}
});
min_max(&minr, &maxr)
}
error[E0425]: cannot find value `i` in this scope
--> src/main.rs:31:34
|
31 | for j in (chunks[i].0)..=(chunks[i].1) {
| ^ help: a local variable with a similar name exists: `a`
Note how the access minr[i]
becomes *minr_i
now, where we’ll have to dereference the reference the zip
ped iterator gave us.
We don’t have i
anymore, but we can get it back via Iterator::enumerate
, if we want to. We could also use yet-another usage of zip
to use an iterator over chunks
, too, alternatively. To stay “closer” to your original code, and to demonstrate something different, I’ll use enumerate
, but the other approach would probably usually be preferred by many Rust users. For zipping more than 2 things, there’s also convenient macros in other crates like: izip in itertools - Rust.
fn extremas(x: &[u64], chunks: &Vec<(usize, usize)>, k: usize) -> (u64, u64) {
let mut minr = vec![0_u64; k];
let mut maxr = vec![0_u64; k];
thread::scope(|s| {
for (i, (minr_i, maxr_i)) in std::iter::zip(&mut minr, &mut maxr).enumerate() {
s.spawn(|| {
let mut a = x[0];
let mut b = x[0];
for j in (chunks[i].0)..=(chunks[i].1) {
if x[j] > b {
b = x[j];
} else if x[j] < a {
a = x[j];
}
}
*minr_i = a;
*maxr_i = b;
});
}
});
min_max(&minr, &maxr)
}
error[E0373]: closure may outlive the current function, but it borrows `i`, which is owned by the current function
--> src/main.rs:28:21
|
26 | thread::scope(|s| {
| - has type `&'1 Scope<'1, '_>`
27 | for (i, (minr_i, maxr_i)) in std::iter::zip(&mut minr, &mut maxr).enumerate() {
28 | s.spawn(|| {
| ^^ may outlive borrowed value `i`
...
31 | for j in (chunks[i].0)..=(chunks[i].1) {
| - `i` is borrowed here
|
note: function requires argument type to outlive `'1`
--> src/main.rs:28:13
|
28 | / s.spawn(|| {
29 | | let mut a = x[0];
30 | | let mut b = x[0];
31 | | for j in (chunks[i].0)..=(chunks[i].1) {
... |
39 | | *maxr_i = b;
40 | | });
| |______________^
help: to force the closure to take ownership of `i` (and any other referenced variables), use the `move` keyword
|
28 | s.spawn(move || {
| ++++
Unlike the trickery that I’ve discussed in your previous thread, for capturing an index in the closure, like i
, there’s no (good) way around converting our code into a move
closure, so we’ll do that, as the compiler suggests.
fn extremas(x: &[u64], chunks: &Vec<(usize, usize)>, k: usize) -> (u64, u64) {
let mut minr = vec![0_u64; k];
let mut maxr = vec![0_u64; k];
thread::scope(|s| {
for (i, (minr_i, maxr_i)) in std::iter::zip(&mut minr, &mut maxr).enumerate() {
s.spawn(move || {
let mut a = x[0];
let mut b = x[0];
for j in (chunks[i].0)..=(chunks[i].1) {
if x[j] > b {
b = x[j];
} else if x[j] < a {
a = x[j];
}
}
*minr_i = a;
*maxr_i = b;
});
}
});
min_max(&minr, &maxr)
}
And voilà , we’re already finished. That was even more hassle-free than anticipated 