Skip to main content

Exec

Struct Exec 

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), and fork_sized hands out explicit shares that sum to at most 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 threadteam::{Exec, Pool};

let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).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::<threadteam::Exec<'static>>();
ⓘ
fn needs_copy<T: Copy>() {}
needs_copy::<threadteam::Exec<'static>>();
ⓘ
let exec = threadteam::Exec::serial();
exec.fork(1, |_, _| {});
exec.fork(1, |_, _| {}); // `fork` consumed the token

Implementations§

§

impl Exec<'static>

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 threadteam::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));
§

impl<'p> Exec<'p>

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 threadteam::{Error, Exec, Pool};

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

pub fn budget(&self) -> NonZero<usize>

The number of slots this token stands for.

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

pub fn dispatch_cost(&self, width: NonZero<usize>) -> Duration

The cost of one parallel section of width participants started now on this token: the steady state (sections back to back, shares that carry work). Duration::MAX when no section is possible: a serial token, or width < 2. width is clamped to the budget.

The kind of token is decided at call time:

  • the token has team authority (it comes from a Team, or is a child of such a token): [DispatchKind::Team];
  • a plain token whose caller is a worker of its pool: [DispatchKind::InPool];
  • a plain token whose caller is outside the pool: [DispatchKind::Outside].

The numbers come from the pool’s DispatchCosts (see PoolBuilder::dispatch_costs). The call allocates nothing and reads no thread-local state of this crate. The answer depends on the calling thread, so ask it on the thread that will dispatch.

§Examples
use std::num::NonZeroUsize;
use std::time::Duration;
use threadteam::Exec;

let two = NonZeroUsize::new(2).unwrap();
assert_eq!(Exec::serial().dispatch_cost(two), Duration::MAX);

pub fn pays( &self, estimate: Duration, sections: u32, width: NonZero<usize>, ) -> bool

Whether a region of serial time estimate that opens sections parallel sections of width participants pays on this token: estimate >= PAYS * sections * dispatch_cost(width) (see [pays_at]). False for a serial token.

§Examples
use std::time::Duration;
use threadteam::Exec;

let two = 2.try_into().unwrap();
assert!(!Exec::serial().pays(Duration::from_secs(1), 1, two));

pub fn width_for(&self, estimate: Duration, sections: u32) -> NonZero<usize>

The width in 1..=budget that minimises sections * D(k) + estimate / k for this token, counting only widths at which the region pays; 1 when none does (see [width_for_by]).

§Examples
use std::time::Duration;
use threadteam::Exec;

assert_eq!(Exec::serial().width_for(Duration::from_secs(1), 1).get(), 1);

pub fn width_for_split(&self, split: SplitCost) -> NonZero<usize>

width_for with the transfer term of a section whose shares read data this thread has just produced: the width in 1..=budget that minimises sections * D(k) + estimate / k + moved(k) / rate, where rate is [TransferRate::host_default] (see [width_for_split_by]).

Use it where the caller knows the bytes it wrote just before the region. A region whose input is not caller-resident passes resident_bytes: 0 and gets width_for exactly.

§Examples
use std::time::Duration;
use threadteam::{Exec, SplitCost};

let split = SplitCost {
    estimate: Duration::from_secs(1),
    sections: 1,
    resident_bytes: 1 << 20,
};
assert_eq!(Exec::serial().width_for_split(split).get(), 1);

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!(threadteam::Exec::serial().is_serial());

pub fn carries_team(&self) -> bool

Whether this token carries team authority: it was minted by a Team or is a child of such a token. Its sections are team sections ([DispatchKind::Team]) and team returns the carried (sub)team at once, without acquisition, so a consumer that wants a [TeamExec] (faer’s Par::Team) asks this first and keeps external for plain tokens, whose team would reserve members.

§Examples
use threadteam::{Exec, Pool};

assert!(!Exec::serial().carries_team());
let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
assert!(!Exec::new(&pool, 2)?.carries_team());
if let Ok(team) = pool.into_team(2) {
    assert!(team.exec(2).carries_team());
}

pub fn limit(self, max: NonZero<usize>) -> 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 threadteam::{Exec, Pool};

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

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 threadteam::Exec;

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

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 threadteam::{Exec, Pool};

let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).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));

pub fn fork_sized<F>(self, shares: &[usize], f: F) -> Result<(), Error>
where F: Fn(usize, Exec<'_>) + Sync,

Runs one child per entry of shares; child i gets a token of exactly shares[i] slots.

For work of uneven size: give a large item several slots and the small ones one slot each. Child i always runs job i (there is no shared counter), and join, panic propagation and placement are those of fork, which is the even special case of this call. One child, or a serial token, runs inline on the calling thread without any scheduler call. An empty shares does nothing. Returns when every child has finished.

Leftover slots. When the shares sum to less than the budget, the remaining slots are given to no child and stay idle for the call: no task runs on them and, on a team token, their reserved members stay parked. They remain the parent’s: fork through reborrow and the parent token has its full budget again after the join.

§Errors

Nothing runs and the token is consumed when

  • a share is zero: [Error::ZeroBudget];
  • the shares sum to more than the budget: [Error::SharesExceedBudget], naming the budget and the sum. Shares are never clamped.
§Panics

Re-raises a panic from any child after every child has finished.

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

let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
let mut exec = Exec::new(&pool, 4)?;
let budget = exec.budget().get(); // 1 without the `parallel` feature
if budget == 4 {
    // One large item on three slots, one small item on the last slot.
    exec.reborrow().fork_sized(&[3, 1], |i, child| {
        assert_eq!(child.budget().get(), [3, 1][i]);
    })?;
}
let err = exec.fork_sized(&[budget, 1], |_, _| {}).unwrap_err();
assert_eq!(err, Error::SharesExceedBudget { budget, sum: budget + 1 });

pub fn fork_sized_slots<S, F>( self, shares: &[usize], slots: &mut [S], f: F, ) -> Result<(), Error>
where S: Send, F: Fn(usize, &mut S, Exec<'_>) + Sync,

fork_sized where child i also gets exclusive &mut access to slots[i], its own state (a cursor, scratch, an error slot), with no lock.

slots stays exclusively borrowed by the call; each child derives its own &mut slots[i] from its index right before calling f, and fork_sized runs each index exactly once, so no two children ever hold the same slot. The borrow of slots ends when every child has joined: while the call runs, neither a sibling nor the caller can reach slot i, and the reference a child receives cannot outlive its call (see docs/design/placement.md). Splitting, placement, join, panic propagation and the share checks are those of fork_sized, which this call goes through.

§Errors

Nothing runs, no slot is touched and the token is consumed when

  • shares.len() != slots.len(): [Error::SlotsMismatch], naming both counts (no slot is silently left out);
  • otherwise, as fork_sized: a zero share is [Error::ZeroBudget], shares summing past the budget are [Error::SharesExceedBudget].
§Panics

Re-raises a panic from any child after every child has finished, as fork_sized does. By then the borrow of slots has ended, so a caller that catches the panic can use every slot again. Each slot holds a valid S: a child’s slot holds what the child last wrote, which after a panic in child i may be the middle of an update to slot i that the child did not finish (nothing is rolled back; validity, not consistency, is what unwinding guarantees, hence the AssertUnwindSafe a catching caller writes).

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

let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
let exec = Exec::new(&pool, 4)?;
let shares: &[usize] = if exec.budget().get() == 4 { &[3, 1] } else { &[1] };
// One cursor per child, advanced without a lock.
let mut cursors = vec![0usize; shares.len()];
exec.fork_sized_slots(shares, &mut cursors, |i, cursor, child| {
    *cursor += 10 * (i + 1) + child.budget().get();
})?;
assert_eq!(cursors[0], 10 + shares[0]);

let err = Exec::serial().fork_sized_slots(&[1], &mut cursors[..0], |_, _, _| {});
assert_eq!(err.unwrap_err(), Error::SlotsMismatch { shares: 1, slots: 0 });

A slot reference cannot escape its child:

ⓘ
use std::sync::Mutex;
let mut slots = [0u32];
let kept: Mutex<Option<&mut u32>> = Mutex::new(None);
threadteam::Exec::serial()
    .fork_sized_slots(&[1], &mut slots, |_, slot, _| *kept.lock().unwrap() = Some(slot))
    .unwrap();

pub fn for_ranges<F>(self, len: usize, grain: NonZero<usize>, 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 threadteam::{Exec, Pool};

let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).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);

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.

  • [Refused::CallerInPool]: the calling thread is a worker of the token’s pool and would occupy a slot.
§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 threadteam::{Exec, Pool};

let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).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");

pub fn can_broadcast(&self) -> bool

Whether broadcast would run right now, without consuming the token.

True exactly when broadcast would not return [Refused]: the token is not derived from fork and the calling thread is not a worker of its pool. A serial or budget-one token returns true, because broadcast then runs f(0) inline. The answer depends on the calling thread, so ask it on the thread that will broadcast.

Use it before building an expensive setup that only a broadcast needs, instead of paying for the setup and then being refused.

§Examples
use threadteam::{Exec, Pool};

let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
let exec = Exec::new(&pool, 2)?;
assert!(exec.can_broadcast());
exec.fork(1, |_, child| {
    let predicted = child.can_broadcast();
    assert_eq!(child.broadcast(|_| {}).is_ok(), predicted);
});
assert!(Exec::serial().can_broadcast());

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

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.

§Placement

The token’s reserved members are lent for the call: each restores its own affinity mask and serves the pool’s jobs until the call returns. The calling thread does the same when it is itself a pinned participant of this pool (for example a member that leads a fork child), so the foreign work never keeps a team’s single-CPU placement. Both re-pin at the next pin section, that is at the next root team scope.

§Examples
use threadteam::Exec;

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

pub fn team<R>( &mut self, f: impl for<'team> FnOnce(TeamExec<'team>) -> R, ) -> Result<R, Refused>

Reserves budget - 1 pool members for the scope of f and hands f the exclusive [TeamExec] authority over them and the calling thread.

The calling thread is participant 0 (the leader); the members are pool workers that stay reserved, spinning and serving no Rayon job, until f returns (or unwinds). A serial or budget-one token yields a team of one without touching the pool.

Acquisition never waits for a Rayon job that is already running. Idle members of an earlier team (see [WaitPolicy]) are claimed with a compare-and-swap; missing ones are requested from the pool, and the call waits at most the pool’s acquisition timeout (PoolBuilder::acquire_timeout) for idle workers to wake. If not all members are there by then, every tentative claim is released and nothing runs. Before f is called, every participant has acknowledged on its own thread.

The token stays with the caller (this takes &mut self), so a refused acquisition leaves it available for a barrier-free fallback such as fork. It is conserved as usual: while f runs the token is borrowed.

This call opens a root team scope: it reads the calling thread’s affinity mask, which decides two things. With PoolBuilder::pin_participants on (the default) it places every member on its own CPU of that mask and pins the leader to the first CPU of the mask until the scope ends (a vendor call from the leader goes through [TeamExec::external], which widens it for that call). A scoped acquisition is placed for the scope; a root token of a Team with the whole team’s budget places the team’s members until a later such scope re-places or unpins them or the Team ends; on the thread that created the Team, the leader is pinned by the team for its whole life, and the scope places the members in the set the team pinned it in. A root token of a Team with a smaller budget changes no placement, pins no leader and runs under the team’s current placement. A mask with fewer CPUs than participants places nobody (Pool::pin_skips counts it). Whether or not pinning is on, it records whether the scope is oversubscribed (more participants than CPUs in the mask), which decides how the members park and how a joining leader waits. See the design record.

§Errors

[Refused::Unavailable] when the members could not be reserved.

§Examples
use threadteam::{Exec, Pool, Refused};

let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
let mut exec = Exec::new(&pool, 4)?;
match exec.team(|team| team.size().get()) {
    Ok(n) => assert!(n >= 1),
    Err(Refused::Unavailable) => exec.fork(4, |_, _| {}), // barrier-free fallback
    Err(_) => unreachable!(),
}

pub fn team_with<R>( &mut self, over: WaitOverride, f: impl for<'team> FnOnce(TeamExec<'team>) -> R, ) -> Result<R, Refused>

team with a scoped override of the waiting policy.

The override applies to this operation only and is inherited by the children of the team; the pool’s defaults are not changed. It governs the idle interval after this reservation ends and never weakens the reservation itself.

§Errors

[Refused::Unavailable] when the members could not be reserved.

Trait Implementations§

§

impl Debug for Exec<'_>

§

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

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
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

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

Initializes a with the given initializer. Read more
§

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

Dereferences the given pointer. Read more
§

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

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

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

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

§

fn simd_from(_simd: S, value: T) -> T

§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

§

fn simd_into(self, simd: S) -> T

Source§

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

Source§

type Error = Infallible

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

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

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.