Threads
Spawn a short-lived thread
std::thread::scope spawns a new scoped thread that is guaranteed
to terminate before returning from the closure, meaning that
you can reference data from the calling function without needing Arc or other ownership tricks.
This recipe splits the array in half and performs the work in separate threads.
fn main() {
let arr = &[1, 25, -4, 10];
let max = find_max(arr);
assert_eq!(max, Some(25));
}
fn find_max(arr: &[i32]) -> Option<i32> {
const THRESHOLD: usize = 2;
if arr.len() <= THRESHOLD {
return arr.iter().cloned().max();
}
let mid = arr.len() / 2;
let (left, right) = arr.split_at(mid);
std::thread::scope(|s| {
let thread_l = s.spawn(|| find_max(left));
let thread_r = s.spawn(|| find_max(right));
let max_l = thread_l.join().unwrap()?;
let max_r = thread_r.join().unwrap()?;
Some(max_l.max(max_r))
})
}
Create a parallel pipeline
This recipe uses the crossbeam and crossbeam-channel crates to create a parallel pipeline, similar to that described in the ZeroMQ guide There is a data source and a data sink, with data being processed by two worker threads in parallel on its way from the source to the sink.
We use bounded channels with a capacity of one using
crossbeam_channel::bounded. The producer must be on its own thread because
it produces messages faster than the workers can process them (since they sleep
for half a second) - this means the producer blocks on the call to
[crossbeam_channel::Sender::send] for half a second until one of the workers
processes the data in the channel. Also note that the data in the channel is
consumed by whichever worker calls receive first, so each message is delivered
to a single worker rather than both workers.
Reading from the channels via the iterator
crossbeam_channel::Receiver::iter method will block, either waiting
for new messages or until the channel is closed. Because the channels were
created within the crossbeam::scope, we must manually close them via drop
to prevent the entire program from blocking on the worker for-loops. You can
think of the calls to drop as signaling that no more messages will be sent.
use crossbeam::channel::bounded;
use std::thread;
use std::time::Duration;
fn main() {
let (snd1, rcv1) = bounded(1);
let (snd2, rcv2) = bounded(1);
let n_msgs = 4;
let n_workers = 2;
crossbeam::scope(|s| {
// Producer thread
s.spawn(|_| {
for i in 0..n_msgs {
snd1.send(i).unwrap();
println!("Source sent {}", i);
}
// Close the channel - this is necessary to exit
// the for-loop in the worker
drop(snd1);
});
// Parallel processing by 2 threads
for _ in 0..n_workers {
// Send to sink, receive from source
let (sendr, recvr) = (snd2.clone(), rcv1.clone());
// Spawn workers in separate threads
s.spawn(move |_| {
thread::sleep(Duration::from_millis(500));
// Receive until channel closes
for msg in recvr.iter() {
println!("Worker {:?} received {}.", thread::current().id(), msg);
sendr.send(msg * 2).unwrap();
}
});
}
// Close the channel, otherwise sink will never
// exit the for-loop
drop(snd2);
// Sink
for msg in rcv2.iter() {
println!("Sink received {}", msg);
}
})
.unwrap();
}
Pass data between two threads
This recipe demonstrates the use of crossbeam-channel in a single producer, single
consumer (SPSC) setting. We build off the ex-crossbeam-spawn recipe by using
crossbeam::scope and Scope::spawn to manage the producer thread. Data is
exchanged between the two threads using a crossbeam_channel::unbounded
channel, meaning there is no limit to the number of storeable messages. The
producer thread sleeps for half a second in between messages.
use crossbeam::channel::unbounded;
use std::{thread, time};
fn main() {
let (snd, rcv) = unbounded();
let n_msgs = 5;
crossbeam::scope(|s| {
s.spawn(|_| {
for i in 0..n_msgs {
snd.send(i).unwrap();
thread::sleep(time::Duration::from_millis(100));
}
});
})
.unwrap();
for _ in 0..n_msgs {
let msg = rcv.recv().unwrap();
println!("Received {}", msg);
}
}
Maintain global mutable state
Declare global state using a static Mutex (also see RwLock).
Mutex::new is a const fn, so no lazy initialization is needed for
an empty collection. If the initial value cannot be computed at compile time,
wrap it in a LazyLock, as in static X: LazyLock<Mutex<T>>.
The Mutex wrap ensures
the state cannot be simultaneously accessed by multiple threads, preventing
race conditions. A MutexGuard must be acquired to read or mutate the
value stored in a Mutex.
use anyhow::{Result, anyhow};
use std::sync::Mutex;
static FRUIT: Mutex<Vec<String>> = Mutex::new(Vec::new());
fn insert(fruit: &str) -> Result<()> {
let mut db = FRUIT
.lock()
.map_err(|_| anyhow!("Failed to acquire MutexGuard"))?;
db.push(fruit.to_string());
Ok(())
}
fn main() -> Result<()> {
insert("apple")?;
insert("orange")?;
insert("peach")?;
{
let db = FRUIT
.lock()
.map_err(|_| anyhow!("Failed to acquire MutexGuard"))?;
db.iter()
.enumerate()
.for_each(|(i, item)| println!("{}: {}", i, item));
}
insert("grape")?;
Ok(())
}
Calculate SHA256 sum of iso files concurrently
rayon walkdir ring ConcurrencyFilesystem
This recipe calculates the SHA256 for every file with iso extension in the
current directory. Walkdir::new iterates the directory and the matching
paths are collected into a Vec. par_iter then spreads the work of reading
each file and computing its hash across rayon’s global thread pool, which has
one thread per available core by default. The digests are gathered with
collect into a single Result, so the first error stops the run.
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use ring::digest::{Context, Digest, SHA256};
use std::fs::File;
use std::io::{BufReader, Error, Read};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
// Verify the iso extension
fn is_iso(entry: &Path) -> bool {
match entry.extension() {
Some(e) if e.to_string_lossy().to_lowercase() == "iso" => true,
_ => false,
}
}
fn compute_digest<P: AsRef<Path>>(filepath: P) -> Result<Digest, Error> {
let mut buf_reader = BufReader::new(File::open(filepath)?);
let mut context = Context::new(&SHA256);
let mut buffer = [0; 1024];
loop {
let count = buf_reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context.finish())
}
fn main() -> Result<(), Error> {
let paths: Vec<PathBuf> = WalkDir::new("/home/user/Downloads")
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| !e.path().is_dir() && is_iso(e.path()))
.map(|e| e.into_path())
.collect();
let digests = paths
.par_iter()
.map(|path| compute_digest(path).map(|digest| (digest, path)))
.collect::<Result<Vec<_>, Error>>()?;
for (sha, path) in digests {
println!("{:?} {:?}", sha, path);
}
Ok(())
}
Sum the size of every file in a directory
rayon walkdir ConcurrencyFilesystem
This recipe adds up the size of every file under the book’s src directory.
Reading a file blocks while the operating system fetches its bytes, so the work
is IO-bound: each thread spends most of its time waiting on the disk rather than
computing. A fixed-size pool keeps several reads in flight at once.
WalkDir yields every entry under src. A rayon::ThreadPool is built with
ThreadPoolBuilder::num_threads set to
std::thread::available_parallelism. ThreadPool::install runs the closure
inside that pool, so the par_iter below uses exactly those threads instead of
rayon’s global pool. Each file is read with std::fs::read, and the byte count
(or the io::Error if the read fails) is collected into a single
io::Result.
Reading the contents is what keeps the workers busy with real IO;
std::fs::metadata would be cheaper when only the length matters.
use anyhow::Result;
use rayon::ThreadPoolBuilder;
use rayon::prelude::{IntoParallelRefIterator, ParallelIterator};
use std::fs;
use std::io;
use std::path::PathBuf;
use std::thread::available_parallelism;
use walkdir::WalkDir;
fn main() -> Result<()> {
let pool = ThreadPoolBuilder::new()
.num_threads(available_parallelism()?.get())
.build()?;
let paths: Vec<PathBuf> = WalkDir::new("src")
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.path().is_file())
.map(|e| e.into_path())
.collect();
let sizes = pool.install(|| {
paths
.par_iter()
.map(|path| fs::read(path).map(|bytes| bytes.len()))
.collect::<io::Result<Vec<usize>>>()
})?;
let total: usize = sizes.iter().sum();
println!("{} files, {} bytes", paths.len(), total);
Ok(())
}
Draw fractal dispatching work to a thread pool
rayon num image ConcurrencyScienceRendering
This recipe generates an image by drawing a fractal from the Julia set with rayon’s thread pool for distributed computation.
Allocate memory for output image of given width and height with ImageBuffer::new.
wavelength_to_rgb calculates RGB pixel values.
par_enumerate_pixels_mut hands rayon every pixel of the image along with
its coordinates, and rayon distributes them over its global thread pool, which
has one thread per available core by default. Each worker writes its pixels
directly into the buffer, so no channel is needed.
ImageBuffer::save writes the image to output.png.
use anyhow::Result;
use image::{ImageBuffer, Rgb};
use num::complex::Complex;
use rayon::prelude::ParallelIterator;
fn main() -> Result<()> {
let (width, height) = (1920, 1080);
let mut img: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(width, height);
let iterations = 300;
let c = Complex::new(-0.8, 0.156);
img.par_enumerate_pixels_mut().for_each(|(x, y, pixel)| {
let i = julia(c, x, y, width, height, iterations);
*pixel = wavelength_to_rgb(380 + i * 400 / iterations);
});
img.save("output.png")?;
Ok(())
}
// Function converting intensity values to RGB
// Based on http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm
fn wavelength_to_rgb(wavelength: u32) -> Rgb<u8> {
let wave = wavelength as f32;
let (r, g, b) = match wavelength {
380..=439 => ((440. - wave) / (440. - 380.), 0.0, 1.0),
440..=489 => (0.0, (wave - 440.) / (490. - 440.), 1.0),
490..=509 => (0.0, 1.0, (510. - wave) / (510. - 490.)),
510..=579 => ((wave - 510.) / (580. - 510.), 1.0, 0.0),
580..=644 => (1.0, (645. - wave) / (645. - 580.), 0.0),
645..=780 => (1.0, 0.0, 0.0),
_ => (0.0, 0.0, 0.0),
};
let factor = match wavelength {
380..=419 => 0.3 + 0.7 * (wave - 380.) / (420. - 380.),
701..=780 => 0.3 + 0.7 * (780. - wave) / (780. - 700.),
_ => 1.0,
};
let (r, g, b) = (
normalize(r, factor),
normalize(g, factor),
normalize(b, factor),
);
Rgb([r, g, b])
}
// Maps Julia set distance estimation to intensity values
fn julia(c: Complex<f32>, x: u32, y: u32, width: u32, height: u32, max_iter: u32) -> u32 {
let width = width as f32;
let height = height as f32;
let mut z = Complex {
// scale and translate the point to image coordinates
re: 3.0 * (x as f32 - 0.5 * width) / width,
im: 2.0 * (y as f32 - 0.5 * height) / height,
};
let mut i = 0;
for t in 0..max_iter {
if z.norm() >= 2.0 {
break;
}
z = z * z + c;
i = t;
}
i
}
// Normalizes color intensity values within RGB range
fn normalize(color: f32, factor: f32) -> u8 {
((color * factor).powf(0.8) * 255.) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn out_of_visible_range_is_black() {
assert_eq!(wavelength_to_rgb(100), Rgb([0, 0, 0]));
}
#[test]
fn julia_iteration_is_bounded() {
let c = Complex::new(-0.8, 0.156);
assert!(julia(c, 960, 540, 1920, 1080, 300) < 300);
}
}
