What is an efficient pattern for concurrently processing an unknown but finite number of jobs?

Viewed 62

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;
    });
}

Playground

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);
        }
    });
}

Playground

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.

1 Answers

I think @Jmb's comment about sending structured messages is the insight I was missing in my busy loop example, so I'm going to turn it into an answer here. Rewritten using structured messages and iterating over the receiver, the example might look something like the following:

use rand;
use std::sync::mpsc::{self, Sender};
use std::thread;

enum Message {
    NewJob(usize),
    Completed,
}
use Message::*;

fn main() {
    let (tx, rx) = mpsc::channel();

    let mut worker_count = 1usize;
    count_up(0, &tx);
    for msg in rx {
        match msg {
            NewJob(from) => {
                println!("{}", from);
                worker_count += 1;
                count_up(from, &tx);
            }
            Completed => worker_count -= 1,
        }
        if worker_count == 0 {
            break;
        }
    }
}

fn count_up(from: usize, tx: &Sender<Message>) {
    let tx = tx.clone();
    thread::spawn(move || {
        let cap: u8 = rand::random();
        if from < cap as usize {
            tx.send(NewJob(from + 1)).expect("message was sent");
        }
        tx.send(Completed).expect("message was sent");
    });
}

Playground

Related