When we have multiple jobs communicating via mpsc with a parent thread, there are various strategies for program termination. When the last job is known, as in this example from the book, we can move the channel transmitter into the last thread and then iterate over the receiver. The iteration will terminate after all jobs have terminated because there will be no remaining references to the transmitter. Similarly, if we know the number of jobs n_jobs upfront, as in this example from the threadpool docs, we can take n_jobs results explicitly from the receiver and process them. Finally, in a server application, we can simply listen to the receiver forever and avoid detecting termination conditions altogether.
However, it's less clear what to do when we have an as yet unknown but finite number of jobs to process.
As a concrete example, imagine a graph with a known starting vertex start and an initially unknown, finite number of vertices reachable from start. A worker is spawned to walk the graph from start and e.g. compute shortest paths to reachable vertices. Each time it discovers a previously unseen reachable vertex reachable, it sends reachable back to the main thread via a channel. Meanwhile, the main thread listens to the channel and spawns a new worker from a pool whenever it receives reachable. Eventually, all reachable vertices have been found and processed, and the program terminates.
What is an efficient pattern for the main thread to both listen to the channel and terminate when finished? The best I've been able to come up with is a busy loop that checks the receiver, checks the worker count (as maintained in a Mutex), and then sleeps or terminates as appropriate, as demonstrated in the following contrived example:
use rand;
use std::sync::mpsc::{self, Sender};
use std::sync::{Arc, Mutex, MutexGuard};
use std::thread;
use std::time::Duration;
fn main() {
let (tx, rx) = mpsc::channel();
let count_arc = Arc::new(Mutex::new(0));
count_up(0, &count_arc, count_arc.lock().unwrap(), &tx);
loop {
{
let count = count_arc.lock().unwrap();
let result = rx.try_recv();
if let Ok(val) = result {
println!("{}", val);
count_up(val, &count_arc, count, &tx);
} else if *count == 0 {
break;
}
}
thread::sleep(Duration::from_millis(10));
}
}
fn count_up(
from: usize,
count_arc: &Arc<Mutex<usize>>,
mut count: MutexGuard<usize>,
tx: &Sender<usize>,
) {
let count_arc = count_arc.clone();
*count += 1;
let tx = tx.clone();
thread::spawn(move || {
let cap: u8 = rand::random();
if from < cap as usize {
tx.send(from + 1).expect("message was sent");
}
*count_arc.lock().unwrap() -= 1;
});
}
Another much simpler option is to recursively spawn threads as in the following:
use std::sync::mpsc::{self, Sender};
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
count_up(0, tx);
for msg in rx {
println!("{}", msg);
}
}
fn count_up(from: usize, tx: Sender<usize>) {
thread::spawn(move || {
let cap: u8 = rand::random();
if from < cap as usize {
tx.send(from + 1).expect("message was sent");
count_up(from + 1, tx);
}
});
}
However, I prefer the idea of a manager parent to a recursive child.
Lest the unbounded look of my examples raise anyone's ire, please note that I would of course use a thread pool in any real code.