Skip to main content

PoolBuilder

Struct PoolBuilder 

Source
pub struct PoolBuilder { /* private fields */ }
Expand description

Configuration of a Pool.

§Examples

use threadteam::Pool;

let pool = Pool::builder()
    .threads(2)
    .stack_size(8 << 20)
    .on_start(|_worker| { /* e.g. pin the thread */ })
    .build()?;
assert!(pool.width().get() >= 1);

Implementations§

Source§

impl PoolBuilder

Source

pub fn threads(self, n: usize) -> PoolBuilder

Number of worker threads. Defaults to the available parallelism.

Zero is rejected by build; the pool never falls back to an environment-dependent default.

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

assert_eq!(Pool::builder().threads(0).build().unwrap_err(), Error::ZeroThreads);
Source

pub fn thread_name(self, f: impl Fn(usize) -> String + 'static) -> PoolBuilder

Names the workers; the closure receives the worker index.

§Examples
let pool = threadteam::Pool::builder()
    .threads(1)
    .thread_name(|i| format!("my-pool-{i}"))
    .build()?;
Source

pub fn stack_size(self, bytes: usize) -> PoolBuilder

Worker stack size in bytes. Defaults to 16 MiB, sized for recursive LU.

§Examples
let pool = threadteam::Pool::builder().threads(1).stack_size(4 << 20).build()?;
Source

pub fn on_start(self, f: impl Fn(usize) + Send + Sync + 'static) -> PoolBuilder

Hook run on each worker when it starts, with the worker index.

Typical use is pinning the worker to a core. The hook is not called without the parallel feature, which starts no workers.

§Examples
let pool = threadteam::Pool::builder().threads(1).on_start(|_| {}).build()?;
Source

pub fn idle_spin(self, spin: Duration) -> PoolBuilder

Default spin time of an unreserved idle team member before it returns its thread to Rayon. Defaults to 200 ms; zero returns it immediately. The member also returns once it has polled for the pool’s spin budget (spin_polls), whichever comes first.

Operations may override it with WaitOverride. A reserved member never leaves on a timeout.

§Examples
use std::time::Duration;

let pool = threadteam::Pool::builder().threads(2).idle_spin(Duration::ZERO).build()?;
assert_eq!(pool.wait_policy().idle_spin(), Duration::ZERO);
Source

pub fn idle_runs_rayon(self, on: bool) -> PoolBuilder

Default for whether unreserved idle members run Rayon jobs while they spin. Defaults to false: native team execution keeps them for the next reservation. true is the compatibility mode in which an idle member serves Rayon work (an acquisition then refuses a member that is inside such a job instead of waiting for it).

§Examples
let pool = threadteam::Pool::builder().threads(2).idle_runs_rayon(true).build()?;
assert!(pool.wait_policy().idle_runs_rayon());
Source

pub fn acquire_timeout(self, timeout: Duration) -> PoolBuilder

How long Exec::team waits for idle workers to wake before it refuses. Defaults to 2 ms.

This bounds the wait for workers that are free; the acquisition does not wait for a worker that is running a job, it only runs out this time and refuses. Raise it on a loaded host to reduce spurious refusals.

§Examples
use std::time::Duration;
let pool = threadteam::Pool::builder()
    .threads(2)
    .acquire_timeout(Duration::from_millis(10))
    .build()?;
Source

pub fn qos(self, qos: Qos) -> PoolBuilder

Scheduling class of the workers (see Qos).

Defaults to Qos::UserInitiated on macOS and to nothing elsewhere. The calling thread’s class is not changed by this crate; an engine that wants its leading thread raised too calls raise_current_thread_qos once on it.

§Examples
use threadteam::{Pool, Qos};
let pool = Pool::builder().threads(1).qos(Qos::Default).build()?;
Source

pub fn dispatch_costs(self, costs: DispatchCosts) -> PoolBuilder

Overrides the DispatchCosts of this pool’s tokens.

Defaults to DispatchCosts::host_default. The costs feed Exec::dispatch_cost and everything built on it (pays, width_for); they never change how work is placed.

§Examples
use std::num::NonZeroUsize;
use std::time::Duration;
use threadteam::{DispatchCosts, DispatchKind, Exec, HostClass, Pool, WidthCosts};

let costs = DispatchCosts::for_class(HostClass::Generic)
    .with(DispatchKind::Outside, WidthCosts::flat(Duration::from_micros(7)));
let pool = Pool::builder().threads(2).dispatch_costs(costs).build()?;
let exec = Exec::new(&pool, 2)?;
// Without the `parallel` feature the pool has width 1: no section, no cost.
let expected = if pool.width().get() >= 2 { Duration::from_micros(7) } else { Duration::MAX };
assert_eq!(exec.dispatch_cost(NonZeroUsize::new(2).unwrap()), expected);
Source

pub fn pin_participants(self, on: bool) -> PoolBuilder

Pins the members of a root team scope onto the calling thread’s CPU set. Off by default; Linux only, where it does something.

With it on, the leader of a root team scope (Exec::team, or the root token of a Team) reads its own affinity mask S with sched_getaffinity, and every member pins itself with sched_setaffinity onto one CPU of S: member k (participant k, counting the leader as 0) onto the k-th set CPU of S, wrapping modulo |S| when the team is wider than the set. S[0] is thereby left to the leader, without enforcing it.

The leader is never pinned. Its mask is never changed, not even for the scope: it keeps all of S while the members work. A vendor library (OpenMP, OpenBLAS, a LAPACK provider) creates its thread team from the thread that calls it, and a team created from a one-CPU thread silently confines the whole provider team to that CPU — measured at 3-20x slower dgemm and LU at 4 threads. Engines call such libraries from the coordinating thread, so that thread must keep its CPU set.

A member saves its previous mask in its own slot and restores it itself before it stops being reserved: before it returns to Rayon, and before its thread serves work that is not its team’s — while it is lent by Exec::external, and when it runs such work itself (a member that leads a fork child and calls external). No thread ever changes another thread’s affinity.

§What it is for

It keeps the members of a team apart. Measured on broccoli (16 threads on 16 allowed CPUs, 20 processes, tensor4all/faer#107): with the scheduler free to place the threads, 11 of 20 processes ran a qr_factor 512 at the one-thread time, with 92 % of the samples showing two runnable team threads on one CPU; with the participants pinned, 0 of 20 did, and no sample was stacked. Giving the team one spare CPU did not help (7 of 20 slow). That arm pinned every participant including the leader; this crate leaves the leader on the caller’s whole set, which is what the rule above requires and only relaxes the placement of participant 0.

§Assumptions and limits
  • Linux only. The CPU set is a stack cpu_set_t and the placement is sched_setaffinity(0, ...). On every other target the option is accepted and does nothing.
  • The caller’s CPU set is the source of placement, so the caller’s mask should belong to this team alone. Two teams whose leaders see the same mask place their members on the same CPUs; give each team (in practice: each process) its own CPU set (taskset -c).
  • Best effort. A refused syscall (a mask larger than cpu_set_t, a cpuset that changed under us, a CPU offlined) leaves that thread where it was; an empty mask pins nobody; a failed pin or restore never fails the scope.
  • A pinned member must not create a foreign thread team. A member that calls OpenMP, OpenBLAS or any library that builds its team from the calling thread directly inside a share confines that library’s team to one CPU. Call it through Exec::external, which restores the member’s mask first, or keep this option off.
  • Only the members of a team are placed. A token of a Team that never opens a team scope (exec.for_ranges(...) directly) is not re-pinned; its members keep the placement of the last root scope (or of Pool::into_team).

Off by default, consistently with the rule above and with libgomp, libomp, OpenBLAS and faer, which leave threads unpinned unless asked.

§Examples
let pool = threadteam::Pool::builder().threads(2).pin_participants(true).build()?;
assert!(pool.pin_participants());
Source

pub fn spin_polls(self, polls: u32) -> PoolBuilder

Spin budget of a team wait, in polls of one PAUSE (YIELD on aarch64): how long a reserved member waiting for work, a leader in a section’s join, or a participant in a barrier spins before it yields a few times and then sleeps until woken (docs/design/waiting.md; the shape of libgomp’s GOMP_SPINCOUNT). A count, so a waiter that is descheduled does not spend it while it waits for its CPU. Zero sleeps after the yields. An oversubscribed scope, or a pool whose waiters were recently descheduled, uses at most 100 polls. It also bounds how long an idle member released by a team keeps polling before it returns to Rayon, together with idle_spin.

§Examples
let pool = threadteam::Pool::builder().threads(2).spin_polls(4096).build()?;
assert_eq!(pool.spin_polls(), 4096);
Source

pub fn build(self) -> Result<Pool, Error>

Starts the workers.

Without the parallel feature no thread is started and the pool has width 1.

§Errors

Error::ZeroThreads when threads(0) was requested, and Error::PoolBuild when the workers cannot be started.

§Examples
let pool = threadteam::Pool::builder().threads(2).build()?;
assert!(pool.width().get() <= 2);

Trait Implementations§

Source§

impl Debug for PoolBuilder

Source§

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

Formats the value using the given formatter. Read more

Auto Trait Implementations§

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, 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.