Skip to main content

Exec

Struct Exec 

Source
pub struct Exec<'p> { /* private fields */ }
Expand description

budget slots of a Pool, or the calling thread alone.

An Exec is the only way kernels start parallel work. It is a token:

  • It is neither Clone nor Copy, and every parallel entry consumes it, so a kernel cannot run two phases at full width at the same time. Use reborrow to run several phases in sequence.
  • Only Exec::new mints a token, and it needs the &Pool; a token never hands its pool back out, so a kernel cannot mint a fresh full-budget token.
  • fork divides the budget among its tasks (shares differ by at most one and sum to the budget), so the number of tasks running at once under one root token is at most its budget at every nesting depth.

A parent waiting for its children keeps no slot.

§Placement

Serial, budget one, or one task: the work runs inline on the calling thread and the scheduler is never called, so no global pool is started. The caller is a worker of the pool: the tasks run as a join tree on the caller, nothing is installed. The caller is a plain thread or a worker of a different pool: the pool is entered once and the caller waits; the tasks run on the target pool. No thread-local state is involved at any point.

§Examples

use std::sync::atomic::{AtomicUsize, Ordering};
use strided_exec::{Exec, Pool};

let pool = Pool::builder().threads(2).build()?;
let exec = Exec::new(&pool, 2)?;
let sum = AtomicUsize::new(0);
exec.for_ranges(100, 1.try_into().unwrap(), |r| {
    sum.fetch_add(r.sum::<usize>(), Ordering::Relaxed);
});
assert_eq!(sum.into_inner(), 4950);

A token cannot be duplicated:

ⓘ
fn needs_clone<T: Clone>() {}
needs_clone::<strided_exec::Exec<'static>>();
ⓘ
fn needs_copy<T: Copy>() {}
needs_copy::<strided_exec::Exec<'static>>();
ⓘ
let exec = strided_exec::Exec::serial();
exec.fork(1, |_, _| {});
exec.fork(1, |_, _| {}); // `fork` consumed the token

Implementations§

Source§

impl Exec<'static>

Source

pub const fn serial() -> Exec<'static>

A token that runs everything inline on the calling thread.

It never touches a scheduler, so using it cannot start any pool.

§Examples
use strided_exec::Exec;

let exec = Exec::serial();
assert!(exec.is_serial());
let here = std::thread::current().id();
exec.fork(3, |_, _| assert_eq!(std::thread::current().id(), here));
Source§

impl<'p> Exec<'p>

Source

pub fn new(pool: &'p Pool, budget: usize) -> Result<Exec<'p>, Error>

A token of budget slots of pool, clamped to the pool’s width.

This is the only place a budget is clamped.

§Errors

Error::ZeroBudget when budget is zero.

§Examples
use strided_exec::{Error, Exec, Pool};

let pool = Pool::builder().threads(2).build()?;
let exec = Exec::new(&pool, 64)?;
assert_eq!(exec.budget(), pool.width());
assert_eq!(Exec::new(&pool, 0).unwrap_err(), Error::ZeroBudget);
Source

pub fn budget(&self) -> NonZeroUsize

The number of slots this token stands for.

§Examples
assert_eq!(strided_exec::Exec::serial().budget().get(), 1);
Source

pub fn is_serial(&self) -> bool

Whether no parallelism is possible: the budget is one slot.

Kernels use this to take their serial path directly.

§Examples
assert!(strided_exec::Exec::serial().is_serial());
Source

pub fn limit(self, max: NonZeroUsize) -> Exec<'p>

Narrows the token to at most max slots.

The result has budget min(budget, max) and the same pool; the dropped slots are simply not used, so slots are conserved. Callers narrow before broadcast or external to match a team size chosen by their own work model. Co-scheduling eligibility is unchanged by limit: a root token stays eligible for broadcast, a child produced by fork stays ineligible.

§Examples
use std::num::NonZeroUsize;
use strided_exec::{Exec, Pool};

let pool = Pool::builder().threads(4).build()?;
let exec = Exec::new(&pool, 4)?.limit(NonZeroUsize::new(2).unwrap());
assert!(exec.budget().get() <= 2);
Source

pub fn reborrow(&mut self) -> Exec<'_>

A token with the same pool and budget, borrowed from self.

Parallel entries consume the token, but a kernel often runs several parallel phases in sequence. Each phase takes a reborrow. The parent is exclusively borrowed and unusable while the child lives, so the slots are held by one token at a time: this conserves slots exactly as handing the token over would.

§Examples
use strided_exec::Exec;

let mut exec = Exec::serial();
exec.reborrow().fork(2, |_, _| {});
exec.reborrow().fork(2, |_, _| {});
exec.fork(2, |_, _| {});
Source

pub fn fork<F>(self, jobs: usize, f: F)
where F: Fn(usize, Exec<'_>) + Sync,

Runs jobs jobs on min(jobs, budget) tasks; job i gets a child token.

The budget is split into one share per task, shares differing by at most one and summing to the budget. A job’s child token carries its task’s share and may be passed to any kernel, which can fork again inside it. When jobs exceeds the budget the tasks take job indices from a shared counter, so the load balances; otherwise job i runs on task i.

One task, a serial token, or jobs <= 1 runs inline on the calling thread without any scheduler call. Returns when every job has finished.

§Examples
use std::sync::atomic::{AtomicUsize, Ordering};
use strided_exec::{Exec, Pool};

let pool = Pool::builder().threads(2).build()?;
let hits: Vec<AtomicUsize> = (0..5).map(|_| AtomicUsize::new(0)).collect();
Exec::new(&pool, 2)?.fork(5, |i, _child| {
    hits[i].fetch_add(1, Ordering::Relaxed);
});
assert!(hits.iter().all(|h| h.load(Ordering::Relaxed) == 1));
Source

pub fn for_ranges<F>(self, len: usize, grain: NonZeroUsize, f: F)
where F: Fn(Range<usize>) + Sync,

Data-parallel loop over 0..len in contiguous ranges.

Uses at most budget tasks; every range is at least grain long except possibly the last, and together the ranges cover 0..len exactly once. When one task suffices (len <= grain, a serial token) f(0..len) runs inline on the calling thread without any scheduler call. len == 0 does nothing.

§Examples
use std::sync::atomic::{AtomicUsize, Ordering};
use strided_exec::{Exec, Pool};

let pool = Pool::builder().threads(2).build()?;
let covered = AtomicUsize::new(0);
Exec::new(&pool, 2)?.for_ranges(10, 4.try_into().unwrap(), |r| {
    covered.fetch_add(r.len(), Ordering::Relaxed);
});
assert_eq!(covered.into_inner(), 10);
Source

pub fn broadcast<F>(self, f: F) -> Result<(), Refused>
where F: Fn(usize) + Sync,

Co-scheduling for barrier-bearing kernels: runs f(i) for every i in 0..budget with all participants running concurrently, or refuses.

A serial or budget-one token runs f(0) inline and returns Ok. Otherwise the pool is entered once and every participant runs on its own worker, so f may block on a barrier shared by the budget participants.

§Errors

Nothing runs and Refused is returned when co-scheduling cannot be guaranteed:

  • Refused::ForkChild: the token was produced by fork (or derived from such a token by reborrow or limit); its slots may share workers with siblings. Roots from Exec::new, and their reborrows and limits, stay eligible.

Callers must handle Refused by falling back to a barrier-free path.

§Guarantee and limits

Concurrent broadcasts on one pool are queued on every worker in the same order, so two of them cannot each hold half the workers. The participants are the pool’s workers 0..budget; if unrelated tasks that never finish occupy those workers, the broadcast waits for them. Keep barrier kernels on a pool whose workers are not held by blocking work of another token.

§Examples
use std::sync::Barrier;
use strided_exec::{Exec, Pool};

let pool = Pool::builder().threads(2).build()?;
let exec = Exec::new(&pool, 2)?;
let barrier = Barrier::new(exec.budget().get());
exec.broadcast(|_i| {
    barrier.wait(); // every participant is running, or this call refused
}).expect("called from a plain thread on a root token");
Source

pub fn external<R: Send>(self, f: impl FnOnce(NonZeroUsize) -> R + Send) -> R

Hands the budget as a plain width to a library outside this stack.

This is the only place a bare width exists. The token is consumed, the pool is entered once (inline when the token is serial, has a budget of one, or the caller already is a worker of the pool) and f receives the budget, for example to build faer::Par::rayon(width) or to set a vendor BLAS thread count. Whatever the library schedules with that width runs on this pool, never on an ambient one. How the library divides the width is its own responsibility. Without the parallel feature, f runs inline and receives a width of one.

§Examples
use strided_exec::Exec;

let width = Exec::serial().external(|width| width.get());
assert_eq!(width, 1);

Trait Implementations§

Source§

impl Debug for Exec<'_>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<'p> !RefUnwindSafe for Exec<'p>

§

impl<'p> !UnwindSafe for Exec<'p>

§

impl<'p> Freeze for Exec<'p>

§

impl<'p> Send for Exec<'p>

§

impl<'p> Sync for Exec<'p>

§

impl<'p> Unpin for Exec<'p>

§

impl<'p> UnsafeUnpin for Exec<'p>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.