Crate forkjoin [] [src]

ForkJoin

A work stealing fork-join parallelism library.

Build Status

Inspired by the blog post Data Parallelism in Rust and implemented as part of a master's thesis. Repository hosted at github.com/faern/forkjoin

Library documentation hosted here

This library has been developed to accommodate the needs of three types of algorithms that all fit very well for fork-join parallelism.

Reduce style

Reduce style is where the algorithm receive an argument, recursively compute a value from this argument and return one answer. Examples of this style include recursively finding the n:th Fibonacci number and summing of tree structures. Characteristics of this style is that the algorithm does not need to mutate its argument and the resulting value is only available after every subtask has been fully computed.

In reduce style algorithms the return values of each subtask is passed to a special join function that is executed when all subtasks have completed. To this join function an extra argument can be sent directly from the task if the algorithm has has ReduceStyle::Arg. This can be seen in the examples here.

Example of reduce style (ReduceStyle::NoArg)

use forkjoin::{TaskResult,ForkPool,AlgoStyle,ReduceStyle,Algorithm};

fn fib_30_with_4_threads() {
    let forkpool = ForkPool::with_threads(4);
    let fibpool = forkpool.init_algorithm(Algorithm {
        fun: fib_task,
        style: AlgoStyle::Reduce(ReduceStyle::NoArg(fib_join)),
    });

    let job = fibpool.schedule(30);
    let result: usize = job.recv().unwrap();
    assert_eq!(1346269, result);
}

fn fib_task(n: usize, _: usize) -> TaskResult<usize, usize> {
    if n < 2 {
        TaskResult::Done(1)
    } else {
        TaskResult::Fork(vec![n-1,n-2], None)
    }
}

fn fib_join(values: &[usize]) -> usize {
    values.iter().fold(0, |acc, &v| acc + v)
}

Example of reduce style (ReduceStyle::Arg)

use forkjoin::{TaskResult,ForkPool,AlgoStyle,ReduceStyle,Algorithm};

struct Tree {
    value: usize,
    children: Vec<Tree>,
}

fn sum_tree(t: &Tree) -> usize {
    let forkpool = ForkPool::new();
    let sumpool = forkpool.init_algorithm(Algorithm {
        fun: sum_tree_task,
        style: AlgoStyle::Reduce(ReduceStyle::Arg(sum_tree_join)),
    });
    let job = sumpool.schedule(t);
    job.recv().unwrap()
}

fn sum_tree_task(t: &Tree, _: usize) -> TaskResult<&Tree, usize> {
    if t.children.is_empty() {
        TaskResult::Done(t.value)
    } else {
        let mut fork_args: Vec<&Tree> = vec![];
        for c in t.children.iter() {
            fork_args.push(c);
        }
        TaskResult::Fork(fork_args, Some(t.value)) // Pass current nodes value to join
    }
}

fn sum_tree_seq(t: &Tree) -> usize {
    t.value + t.children.iter().fold(0, |acc, t2| acc + sum_tree_seq(t2))
}

fn sum_tree_join(value: &usize, values: &[usize]) -> usize {
    *value + values.iter().fold(0, |acc, &v| acc + v)
}

Search style

Search style return results continuously and can sometimes start without any argument, or start with some initial state. The algorithm produce one or multiple output values during the execution, possibly aborting anywhere in the middle. Algorithms where leafs in the problem tree represent a complete solution to the problem (unless the leaf represent a dead end that is not a solution and does not spawn any subtasks), for example nqueens and sudoku solvers, have this style. Characteristics of the search style is that they can produce multiple results and can abort before all tasks in the tree have been computed.

Example of search style

use forkjoin::{ForkPool,TaskResult,AlgoStyle,Algorithm};

type Queen = usize;
type Board = Vec<Queen>;
type Solutions = Vec<Board>;

fn search_nqueens() {
    let n: usize = 8;
    let empty = vec![];

    let forkpool = ForkPool::with_threads(4);
    let queenpool = forkpool.init_algorithm(Algorithm {
        fun: nqueens_task,
        style: AlgoStyle::Search,
    });

    let job = queenpool.schedule((empty, n));

    let mut solutions: Vec<Board> = vec![];
    loop {
        match job.recv() {
            Err(..) => break, // Job has completed
            Ok(board) => solutions.push(board),
        };
    }
    let num_solutions = solutions.len();
    println!("Found {} solutions to nqueens({}x{})", num_solutions, n, n);
}

fn nqueens_task((q, n): (Board, usize), _: usize) -> TaskResult<(Board,usize), Board> {
    if q.len() == n {
        TaskResult::Done(q)
    } else {
        let mut fork_args: Vec<(Board, usize)> = vec![];
        for i in 0..n {
            let mut q2 = q.clone();
            q2.push(i);

            if ok(&q2[..]) {
                fork_args.push((q2, n));
            }
        }
        TaskResult::Fork(fork_args, None)
    }
}

fn ok(q: &[usize]) -> bool {
    for (x1, &y1) in q.iter().enumerate() {
        for (x2, &y2) in q.iter().enumerate() {
            if x2 > x1 {
                let xd = x2-x1;
                if y1 == y2 || y1 == y2 + xd || (y2 >= xd && y1 == y2 - xd) {
                    return false;
                }
            }
        }
    }
    true
}

In-place mutation style

NOTE: This style works in the current lib version, but it requires very ugly unsafe code!

In-place mutation style receive a mutable argument, recursively modifies this value and the result is the argument itself. Sorting algorithms that sort their input arrays are cases of this style. Characteristics of this style is that they mutate their input argument instead of producing any output.

Examples of this will come when they can be nicely implemented.

Tasks

The small units that are executed and can choose to fork or to return a value is the TaskFun. A TaskFun can NEVER block, because that would block the kernel thread it's being executed on. Instead it should decide if it's done calculating or need to fork. This decision is taken in the return value to indicate to the user that a TaskFun need to return before anything can happen.

A TaskFun return a TaskResult. It can be TaskResult::Done(value) if it's done calculating. It can be TaskResult::Fork(args) if it needs to fork.

TODO

Structs

AlgoOnPool

A handle for a specific Algorithm running on a ForkPool. Acquired from ForkPool::init_algorithm.

Algorithm

The representation of a specific algorithm to use the ForkJoin library.

ForkPool

Main struct of the ForkJoin library. Represents a pool of threads implementing a work stealing algorithm.

Job

The handle for a computation. Can be used to fetch results of the computation. Upon drop it will wait for the entire computation to complete if it's still executing. Algorithm termination is detected by the try_recv and recv methods returning a ResultError

JoinBarrier

Internal struct for receiving results from multiple subtasks in parallel

Task

Internal representation of a task.

Enums

AlgoStyle

Enum representing the style of the executed algorithm.

ReduceStyle

Enum indicating what type of join function an Algorithm will use.

ResultError

Enum indicating there was a problem fetching a result from a job.

ResultReceiver

Enum describing what to do with results of Tasks and JoinBarriers.

TaskResult

Return values from tasks. Represent a computed value or a fork of the algorithm.

Type Definitions

TaskFun

Type definition of the main function in a task. Your task functions must have this signature

TaskJoin

Type definition of functions joining together forked results. Only used in AlgoStyle::Reduce algorithms with ReduceStyle::NoArg.

TaskJoinArg

Similar to TaskJoin but takes an extra argument sent directly from the task in algorithms with ReduceStyle::Arg.