Function rayon_core::join

source ·
pub fn join<A, B, RA, RB>(oper_a: A, oper_b: B) -> (RA, RB)where
    A: FnOnce() -> RA + Send,
    B: FnOnce() -> RB + Send,
    RA: Send,
    RB: Send,
Expand description

Takes two closures and potentially runs them in parallel. It returns a pair of the results from those closures.

Conceptually, calling join() is similar to spawning two threads, one executing each of the two closures. However, the implementation is quite different and incurs very low overhead. The underlying technique is called “work stealing”: the Rayon runtime uses a fixed pool of worker threads and attempts to only execute code in parallel when there are idle CPUs to handle it.

When join is called from outside the thread pool, the calling thread will block while the closures execute in the pool. When join is called within the pool, the calling thread still actively participates in the thread pool. It will begin by executing closure A (on the current thread). While it is doing that, it will advertise closure B as being available for other threads to execute. Once closure A has completed, the current thread will try to execute closure B; if however closure B has been stolen, then it will look for other work while waiting for the thief to fully execute closure B. (This is the typical work-stealing strategy).

Examples

This example uses join to perform a quick-sort (note this is not a particularly optimized implementation: if you actually want to sort for real, you should prefer the par_sort method offered by Rayon).

let mut v = vec![5, 1, 8, 22, 0, 44];
quick_sort(&mut v);
assert_eq!(v, vec![0, 1, 5, 8, 22, 44]);

fn quick_sort<T:PartialOrd+Send>(v: &mut [T]) {
   if v.len() > 1 {
       let mid = partition(v);
       let (lo, hi) = v.split_at_mut(mid);
       rayon::join(|| quick_sort(lo),
                   || quick_sort(hi));
   }
}

// Partition rearranges all items `<=` to the pivot
// item (arbitrary selected to be the last item in the slice)
// to the first half of the slice. It then returns the
// "dividing point" where the pivot is placed.
fn partition<T:PartialOrd+Send>(v: &mut [T]) -> usize {
    let pivot = v.len() - 1;
    let mut i = 0;
    for j in 0..pivot {
        if v[j] <= v[pivot] {
            v.swap(i, j);
            i += 1;
        }
    }
    v.swap(i, pivot);
    i
}

Warning about blocking I/O

The assumption is that the closures given to join() are CPU-bound tasks that do not perform I/O or other blocking operations. If you do perform I/O, and that I/O should block (e.g., waiting for a network request), the overall performance may be poor. Moreover, if you cause one closure to be blocked waiting on another (for example, using a channel), that could lead to a deadlock.

Panics

No matter what happens, both closures will always be executed. If a single closure panics, whether it be the first or second closure, that panic will be propagated and hence join() will panic with the same panic value. If both closures panic, join() will panic with the panic value from the first closure.