Skip to main content

threadteam/
exec.rs

1//! The splittable placement token.
2
3use crate::awake::Awake;
4#[cfg(feature = "parallel")]
5use crate::awake::LendGuard;
6use crate::cost::{pays_at, width_for_by, DispatchKind};
7use crate::error::{Error, Refused};
8use crate::policy::{WaitOverride, WaitPolicy};
9use crate::pool::Pool;
10use crate::reserve::LeaderGuard;
11use crate::sched;
12use crate::split;
13use crate::team::{split_raw, spread_raw, Plan, TeamExec};
14use crate::track::Track;
15use core::fmt;
16use core::num::NonZeroUsize;
17use core::ops::Range;
18use core::sync::atomic::{AtomicUsize, Ordering};
19use core::time::Duration;
20
21/// `budget` slots of a [`Pool`], or the calling thread alone.
22///
23/// An `Exec` is the only way kernels start parallel work. It is a *token*:
24///
25/// * It is neither `Clone` nor `Copy`, and every parallel entry consumes it, so
26///   a kernel cannot run two phases at full width at the same time. Use
27///   [`reborrow`](Exec::reborrow) to run several phases in sequence.
28/// * Only [`Exec::new`] mints a token, and it needs the `&Pool`; a token never
29///   hands its pool back out, so a kernel cannot mint a fresh full-budget token.
30/// * [`fork`](Exec::fork) divides the budget among its tasks (shares differ by
31///   at most one and sum to the budget), and [`fork_sized`](Exec::fork_sized)
32///   hands out explicit shares that sum to at most the budget, so the number of
33///   tasks running at once under one root token is at most its budget at every
34///   nesting depth.
35///
36/// A parent waiting for its children keeps no slot.
37///
38/// # Placement
39///
40/// Serial, budget one, or one task: the work runs inline on the calling thread
41/// and the scheduler is never called, so no global pool is started. The caller
42/// is a worker of the pool: the tasks run as a join tree on the caller, nothing is
43/// installed. The caller is a plain thread or a worker of a different pool: the
44/// pool is entered once and the caller waits; the tasks run on the target pool.
45/// No thread-local state is involved at any point.
46///
47/// # Examples
48///
49/// ```
50/// use std::sync::atomic::{AtomicUsize, Ordering};
51/// use threadteam::{Exec, Pool};
52///
53/// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
54/// let exec = Exec::new(&pool, 2)?;
55/// let sum = AtomicUsize::new(0);
56/// exec.for_ranges(100, 1.try_into().unwrap(), |r| {
57///     sum.fetch_add(r.sum::<usize>(), Ordering::Relaxed);
58/// });
59/// assert_eq!(sum.into_inner(), 4950);
60/// # Ok::<(), threadteam::Error>(())
61/// ```
62///
63/// A token cannot be duplicated:
64///
65/// ```compile_fail
66/// fn needs_clone<T: Clone>() {}
67/// needs_clone::<threadteam::Exec<'static>>();
68/// ```
69///
70/// ```compile_fail
71/// fn needs_copy<T: Copy>() {}
72/// needs_copy::<threadteam::Exec<'static>>();
73/// ```
74///
75/// ```compile_fail
76/// let exec = threadteam::Exec::serial();
77/// exec.fork(1, |_, _| {});
78/// exec.fork(1, |_, _| {}); // `fork` consumed the token
79/// ```
80#[must_use = "an Exec is a budget of execution slots; pass it to a kernel"]
81pub struct Exec<'p> {
82    pool: Option<&'p Pool>,
83    budget: NonZeroUsize,
84    /// Produced by `fork` (directly or through `reborrow`).
85    child: bool,
86    track: Track,
87    /// Authority over reserved members, for tokens minted from a [`Team`](crate::Team).
88    team: Option<TeamAuth<'p>>,
89    /// Held by the root token of a `Team`: one leader at a time.
90    _leader: Option<LeaderGuard<'p>>,
91}
92
93/// The reserved members a token may dispatch to; `ids.len() + 1 == budget`.
94#[derive(Clone, Copy)]
95pub(crate) struct TeamAuth<'p> {
96    pub(crate) awake: &'p Awake,
97    pub(crate) ids: &'p [usize],
98    pub(crate) policy: WaitPolicy,
99    /// The root scope of the token is oversubscribed (see [`TeamExec`]).
100    /// Fixed when the root token is minted and carried into `reborrow`,
101    /// `limit`, `fork` children and every section.
102    pub(crate) over: bool,
103}
104
105impl Exec<'static> {
106    /// A token that runs everything inline on the calling thread.
107    ///
108    /// It never touches a scheduler, so using it cannot start any pool.
109    ///
110    /// # Examples
111    ///
112    /// ```
113    /// use threadteam::Exec;
114    ///
115    /// let exec = Exec::serial();
116    /// assert!(exec.is_serial());
117    /// let here = std::thread::current().id();
118    /// exec.fork(3, |_, _| assert_eq!(std::thread::current().id(), here));
119    /// ```
120    pub const fn serial() -> Exec<'static> {
121        Exec {
122            pool: None,
123            budget: NonZeroUsize::MIN,
124            child: false,
125            track: Track::NONE,
126            team: None,
127            _leader: None,
128        }
129    }
130}
131
132impl<'p> Exec<'p> {
133    /// A token of `budget` slots of `pool`, clamped to the pool's width.
134    ///
135    /// This is the only place a budget is clamped.
136    ///
137    /// # Errors
138    ///
139    /// [`Error::ZeroBudget`] when `budget` is zero.
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// use threadteam::{Error, Exec, Pool};
145    ///
146    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
147    /// let exec = Exec::new(&pool, 64)?;
148    /// assert_eq!(exec.budget(), pool.width());
149    /// assert_eq!(Exec::new(&pool, 0).unwrap_err(), Error::ZeroBudget);
150    /// # Ok::<(), Error>(())
151    /// ```
152    pub fn new(pool: &'p Pool, budget: usize) -> Result<Exec<'p>, Error> {
153        let budget = NonZeroUsize::new(budget).ok_or(Error::ZeroBudget)?;
154        Ok(Exec {
155            pool: Some(pool),
156            budget: budget.min(pool.width()),
157            child: false,
158            track: Track::root(),
159            team: None,
160            _leader: None,
161        })
162    }
163
164    /// A root token of a [`Team`](crate::Team): `budget` is already clamped and
165    /// `ids.len() + 1 == budget`.
166    pub(crate) fn from_team(
167        pool: &'p Pool,
168        auth: TeamAuth<'p>,
169        guard: LeaderGuard<'p>,
170    ) -> Exec<'p> {
171        Exec {
172            pool: Some(pool),
173            budget: NonZeroUsize::new(auth.ids.len() + 1).unwrap_or(NonZeroUsize::MIN),
174            child: false,
175            track: Track::root(),
176            team: Some(auth),
177            _leader: Some(guard),
178        }
179    }
180
181    /// The number of slots this token stands for.
182    ///
183    /// # Examples
184    ///
185    /// ```
186    /// assert_eq!(threadteam::Exec::serial().budget().get(), 1);
187    /// ```
188    pub fn budget(&self) -> NonZeroUsize {
189        self.budget
190    }
191
192    /// The cost of one parallel section of `width` participants started now on
193    /// this token: the steady state (sections back to back, shares that carry
194    /// work). [`Duration::MAX`] when no section is possible: a serial token, or
195    /// `width < 2`. `width` is clamped to the budget.
196    ///
197    /// The kind of token is decided at call time:
198    ///
199    /// * the token has team authority (it comes from a [`Team`](crate::Team), or
200    ///   is a child of such a token): [`DispatchKind::Team`];
201    /// * a plain token whose caller is a worker of its pool:
202    ///   [`DispatchKind::InPool`];
203    /// * a plain token whose caller is outside the pool: [`DispatchKind::Outside`].
204    ///
205    /// The numbers come from the pool's [`DispatchCosts`](crate::DispatchCosts) (see
206    /// [`PoolBuilder::dispatch_costs`](crate::PoolBuilder::dispatch_costs)). The
207    /// call allocates nothing and reads no thread-local state of this crate.
208    /// The answer depends on the calling thread, so ask it on the thread that
209    /// will dispatch.
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use std::num::NonZeroUsize;
215    /// use std::time::Duration;
216    /// use threadteam::Exec;
217    ///
218    /// let two = NonZeroUsize::new(2).unwrap();
219    /// assert_eq!(Exec::serial().dispatch_cost(two), Duration::MAX);
220    /// ```
221    pub fn dispatch_cost(&self, width: NonZeroUsize) -> Duration {
222        let width = width.min(self.budget);
223        if width.get() < 2 {
224            return Duration::MAX;
225        }
226        match (self.team, self.pool) {
227            (Some(t), _) => t.awake.costs.cost(DispatchKind::Team, width),
228            (None, Some(pool)) => {
229                let kind = if sched::in_pool(pool) {
230                    DispatchKind::InPool
231                } else {
232                    DispatchKind::Outside
233                };
234                pool.awake.costs.cost(kind, width)
235            }
236            (None, None) => Duration::MAX,
237        }
238    }
239
240    /// Whether a region of serial time `estimate` that opens `sections` parallel
241    /// sections of `width` participants pays on this token: `estimate >=
242    /// PAYS * sections * dispatch_cost(width)` (see [`pays_at`]). False for a
243    /// serial token.
244    ///
245    /// # Examples
246    ///
247    /// ```
248    /// use std::time::Duration;
249    /// use threadteam::Exec;
250    ///
251    /// let two = 2.try_into().unwrap();
252    /// assert!(!Exec::serial().pays(Duration::from_secs(1), 1, two));
253    /// ```
254    pub fn pays(&self, estimate: Duration, sections: u32, width: NonZeroUsize) -> bool {
255        pays_at(estimate, sections, self.dispatch_cost(width))
256    }
257
258    /// The width in `1..=budget` that minimises `sections * D(k) + estimate / k`
259    /// for this token, counting only widths at which the region
260    /// [`pays`](Self::pays); 1 when none does (see [`width_for_by`]).
261    ///
262    /// # Examples
263    ///
264    /// ```
265    /// use std::time::Duration;
266    /// use threadteam::Exec;
267    ///
268    /// assert_eq!(Exec::serial().width_for(Duration::from_secs(1), 1).get(), 1);
269    /// ```
270    pub fn width_for(&self, estimate: Duration, sections: u32) -> NonZeroUsize {
271        width_for_by(estimate, sections, self.budget, |k| self.dispatch_cost(k))
272    }
273
274    /// Whether no parallelism is possible: the budget is one slot.
275    ///
276    /// Kernels use this to take their serial path directly.
277    ///
278    /// # Examples
279    ///
280    /// ```
281    /// assert!(threadteam::Exec::serial().is_serial());
282    /// ```
283    pub fn is_serial(&self) -> bool {
284        self.budget.get() == 1
285    }
286
287    /// Whether this token carries team authority: it was minted by a
288    /// [`Team`](crate::Team) or is a child of such a token. Its sections are team
289    /// sections ([`DispatchKind::Team`]) and [`team`](Exec::team) returns the
290    /// carried (sub)team at once, without acquisition, so a consumer that wants a
291    /// [`TeamExec`] (faer's `Par::Team`) asks this first and keeps
292    /// [`external`](Exec::external) for plain tokens, whose `team` would reserve
293    /// members.
294    ///
295    /// # Examples
296    ///
297    /// ```
298    /// use threadteam::{Exec, Pool};
299    ///
300    /// assert!(!Exec::serial().carries_team());
301    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
302    /// assert!(!Exec::new(&pool, 2)?.carries_team());
303    /// if let Ok(team) = pool.into_team(2) {
304    ///     assert!(team.exec(2).carries_team());
305    /// }
306    /// # Ok::<(), threadteam::Error>(())
307    /// ```
308    pub fn carries_team(&self) -> bool {
309        self.team.is_some()
310    }
311
312    /// Narrows the token to at most `max` slots.
313    ///
314    /// The result has budget `min(budget, max)` and the same pool; the dropped
315    /// slots are simply not used, so slots are conserved. Callers narrow before
316    /// [`broadcast`](Exec::broadcast) or [`external`](Exec::external) to match a team size chosen
317    /// by their own work model. Co-scheduling eligibility is unchanged by
318    /// `limit`: a root token stays eligible for `broadcast`, a child produced by
319    /// [`fork`](Exec::fork) stays ineligible.
320    ///
321    /// # Examples
322    ///
323    /// ```
324    /// use std::num::NonZeroUsize;
325    /// use threadteam::{Exec, Pool};
326    ///
327    /// let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
328    /// let exec = Exec::new(&pool, 4)?.limit(NonZeroUsize::new(2).unwrap());
329    /// assert!(exec.budget().get() <= 2);
330    /// # Ok::<(), threadteam::Error>(())
331    /// ```
332    pub fn limit(self, max: NonZeroUsize) -> Exec<'p> {
333        let budget = self.budget.min(max);
334        Exec {
335            budget,
336            team: self.team.map(|t| TeamAuth {
337                ids: &t.ids[..budget.get() - 1],
338                ..t
339            }),
340            ..self
341        }
342    }
343
344    /// A token with the same pool and budget, borrowed from `self`.
345    ///
346    /// Parallel entries consume the token, but a kernel often runs several
347    /// parallel phases in sequence. Each phase takes a reborrow. The parent is
348    /// exclusively borrowed and unusable while the child lives, so the slots are
349    /// held by one token at a time: this conserves slots exactly as handing the
350    /// token over would.
351    ///
352    /// # Examples
353    ///
354    /// ```
355    /// use threadteam::Exec;
356    ///
357    /// let mut exec = Exec::serial();
358    /// exec.reborrow().fork(2, |_, _| {});
359    /// exec.reborrow().fork(2, |_, _| {});
360    /// exec.fork(2, |_, _| {});
361    /// ```
362    pub fn reborrow(&mut self) -> Exec<'_> {
363        Exec {
364            pool: self.pool,
365            budget: self.budget,
366            child: self.child,
367            track: self.track.alias(),
368            team: self.team,
369            _leader: None,
370        }
371    }
372
373    /// Runs `jobs` jobs on `min(jobs, budget)` tasks; job `i` gets a child token.
374    ///
375    /// The budget is split into one share per task, shares differing by at most
376    /// one and summing to the budget. A job's child token carries its task's
377    /// share and may be passed to any kernel, which can `fork` again inside it.
378    /// When `jobs` exceeds the budget the tasks take job indices from a shared
379    /// counter, so the load balances; otherwise job `i` runs on task `i`.
380    ///
381    /// One task, a serial token, or `jobs <= 1` runs inline on the calling
382    /// thread without any scheduler call. Returns when every job has finished.
383    ///
384    /// # Examples
385    ///
386    /// ```
387    /// use std::sync::atomic::{AtomicUsize, Ordering};
388    /// use threadteam::{Exec, Pool};
389    ///
390    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
391    /// let hits: Vec<AtomicUsize> = (0..5).map(|_| AtomicUsize::new(0)).collect();
392    /// Exec::new(&pool, 2)?.fork(5, |i, _child| {
393    ///     hits[i].fetch_add(1, Ordering::Relaxed);
394    /// });
395    /// assert!(hits.iter().all(|h| h.load(Ordering::Relaxed) == 1));
396    /// # Ok::<(), threadteam::Error>(())
397    /// ```
398    pub fn fork<F>(self, jobs: usize, f: F)
399    where
400        F: Fn(usize, Exec<'_>) + Sync,
401    {
402        if jobs == 0 {
403            return;
404        }
405        let size = self.budget.get();
406        let plan = Plan::Even {
407            size,
408            children: jobs.min(size),
409        };
410        self.fork_plan(&plan, jobs, f);
411    }
412
413    /// Runs one child per entry of `shares`; child `i` gets a token of exactly
414    /// `shares[i]` slots.
415    ///
416    /// For work of uneven size: give a large item several slots and the small
417    /// ones one slot each. Child `i` always runs job `i` (there is no shared
418    /// counter), and join, panic propagation and placement are those of
419    /// [`fork`](Exec::fork), which is the even special case of this call.
420    /// One child, or a serial token, runs inline on the calling thread without
421    /// any scheduler call. An empty `shares` does nothing. Returns when every
422    /// child has finished.
423    ///
424    /// **Leftover slots.** When the shares sum to less than the budget, the
425    /// remaining slots are given to no child and stay idle for the call: no
426    /// task runs on them and, on a team token, their reserved members stay
427    /// parked. They remain the parent's: fork through
428    /// [`reborrow`](Exec::reborrow) and the parent token has its full budget
429    /// again after the join.
430    ///
431    /// # Errors
432    ///
433    /// Nothing runs and the token is consumed when
434    ///
435    /// * a share is zero: [`Error::ZeroBudget`];
436    /// * the shares sum to more than the budget:
437    ///   [`Error::SharesExceedBudget`], naming the budget and the sum. Shares
438    ///   are never clamped.
439    ///
440    /// # Panics
441    ///
442    /// Re-raises a panic from any child after every child has finished.
443    ///
444    /// # Examples
445    ///
446    /// ```
447    /// use threadteam::{Error, Exec, Pool};
448    ///
449    /// let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
450    /// let mut exec = Exec::new(&pool, 4)?;
451    /// let budget = exec.budget().get(); // 1 without the `parallel` feature
452    /// if budget == 4 {
453    ///     // One large item on three slots, one small item on the last slot.
454    ///     exec.reborrow().fork_sized(&[3, 1], |i, child| {
455    ///         assert_eq!(child.budget().get(), [3, 1][i]);
456    ///     })?;
457    /// }
458    /// let err = exec.fork_sized(&[budget, 1], |_, _| {}).unwrap_err();
459    /// assert_eq!(err, Error::SharesExceedBudget { budget, sum: budget + 1 });
460    /// # Ok::<(), Error>(())
461    /// ```
462    pub fn fork_sized<F>(self, shares: &[usize], f: F) -> Result<(), Error>
463    where
464        F: Fn(usize, Exec<'_>) + Sync,
465    {
466        if shares.contains(&0) {
467            return Err(Error::ZeroBudget);
468        }
469        let budget = self.budget.get();
470        let sum = shares.iter().fold(0usize, |a, &s| a.saturating_add(s));
471        if sum > budget {
472            return Err(Error::SharesExceedBudget { budget, sum });
473        }
474        if !shares.is_empty() {
475            self.fork_plan(&Plan::Sizes(shares), shares.len(), f);
476        }
477        Ok(())
478    }
479
480    /// The one splitter of `fork` and `fork_sized`: task `k` of `plan` gets a
481    /// child token of `plan.child(k).1` slots. More `jobs` than tasks pull job
482    /// indices from a shared counter; otherwise job `k` runs on task `k`.
483    fn fork_plan<F>(self, plan: &Plan<'_>, jobs: usize, f: F)
484    where
485        F: Fn(usize, Exec<'_>) + Sync,
486    {
487        let tasks = plan.children();
488        let _released = self.track.release();
489        let next = AtomicUsize::new(0);
490        let this = &self;
491        let body = |k: usize, team_ids: Option<&[usize]>| {
492            let _live = this.track.task();
493            let share = plan.child(k).1;
494            let child = || Exec {
495                pool: this.pool,
496                // Every share of a validated plan is at least one.
497                budget: NonZeroUsize::new(share).unwrap_or(NonZeroUsize::MIN),
498                child: true,
499                track: this.track.child(),
500                team: this
501                    .team
502                    .zip(team_ids)
503                    .map(|(t, ids)| TeamAuth { ids, ..t }),
504                _leader: None,
505            };
506            if jobs <= tasks {
507                f(k, child());
508            } else {
509                loop {
510                    let job = next.fetch_add(1, Ordering::Relaxed);
511                    if job >= jobs {
512                        break;
513                    }
514                    f(job, child());
515                }
516            }
517        };
518        match (self.team, self.pool) {
519            (Some(t), _) if tasks > 1 => {
520                split_raw(Some(t.awake), t.ids, plan, t.over, |k, ids| {
521                    body(k, Some(ids));
522                });
523            }
524            (None, Some(pool)) if tasks > 1 => {
525                sched::run_tasks(pool, tasks, &self.track, |k| body(k, None));
526            }
527            _ => {
528                for k in 0..tasks {
529                    // The members of child `k`, as `split_raw` hands them out.
530                    let (start, size) = plan.child(k);
531                    body(k, self.team.map(|t| &t.ids[start..start + size - 1]));
532                }
533            }
534        }
535    }
536
537    /// Data-parallel loop over `0..len` in contiguous ranges.
538    ///
539    /// Uses at most `budget` tasks; every range is at least `grain` long except
540    /// possibly the last, and together the ranges cover `0..len` exactly once.
541    /// When one task suffices (`len <= grain`, a serial token) `f(0..len)` runs
542    /// inline on the calling thread without any scheduler call. `len == 0` does
543    /// nothing.
544    ///
545    /// # Examples
546    ///
547    /// ```
548    /// use std::sync::atomic::{AtomicUsize, Ordering};
549    /// use threadteam::{Exec, Pool};
550    ///
551    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
552    /// let covered = AtomicUsize::new(0);
553    /// Exec::new(&pool, 2)?.for_ranges(10, 4.try_into().unwrap(), |r| {
554    ///     covered.fetch_add(r.len(), Ordering::Relaxed);
555    /// });
556    /// assert_eq!(covered.into_inner(), 10);
557    /// # Ok::<(), threadteam::Error>(())
558    /// ```
559    pub fn for_ranges<F>(self, len: usize, grain: NonZeroUsize, f: F)
560    where
561        F: Fn(Range<usize>) + Sync,
562    {
563        if len == 0 {
564            return;
565        }
566        let (chunk, tasks) = split::range_plan(len, grain.get(), self.budget.get());
567        let _released = self.track.release();
568        let track = &self.track;
569        let body = |k: usize| {
570            let _live = track.task();
571            f(split::chunk_range(len, chunk, k));
572        };
573        match (self.team, self.pool) {
574            (Some(t), _) if tasks > 1 => {
575                spread_raw(Some(t.awake), t.ids, tasks, t.over, body);
576            }
577            (None, Some(pool)) if tasks > 1 => sched::run_tasks(pool, tasks, track, body),
578            _ => {
579                for k in 0..tasks {
580                    body(k);
581                }
582            }
583        }
584    }
585
586    /// Co-scheduling for barrier-bearing kernels: runs `f(i)` for every
587    /// `i in 0..budget` with all participants running concurrently, or refuses.
588    ///
589    /// A serial or budget-one token runs `f(0)` inline and returns `Ok`.
590    /// Otherwise the pool is entered once and every participant runs on its own
591    /// worker, so `f` may block on a barrier shared by the `budget`
592    /// participants.
593    ///
594    /// # Errors
595    ///
596    /// Nothing runs and [`Refused`] is returned when co-scheduling cannot be
597    /// guaranteed:
598    ///
599    /// * [`Refused::ForkChild`]: the token was produced by [`fork`](Exec::fork)
600    ///   (or derived from such a token by `reborrow` or `limit`); its slots may
601    ///   share workers with siblings. Roots from [`Exec::new`], and their
602    ///   `reborrow`s and `limit`s, stay eligible.
603    ///
604    /// Callers must handle `Refused` by falling back to a barrier-free path.
605    /// * [`Refused::CallerInPool`]: the calling thread is a worker of the
606    ///   token's pool and would occupy a slot.
607    ///
608    /// # Guarantee and limits
609    ///
610    /// Concurrent broadcasts on one pool are queued on every worker in the same
611    /// order, so two of them cannot each hold half the workers. The participants
612    /// are the pool's workers `0..budget`; if unrelated tasks that never finish
613    /// occupy those workers, the broadcast waits for them. Keep barrier kernels on a pool whose workers are not held
614    /// by blocking work of another token.
615    ///
616    /// # Examples
617    ///
618    /// ```
619    /// use std::sync::Barrier;
620    /// use threadteam::{Exec, Pool};
621    ///
622    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
623    /// let exec = Exec::new(&pool, 2)?;
624    /// let barrier = Barrier::new(exec.budget().get());
625    /// exec.broadcast(|_i| {
626    ///     barrier.wait(); // every participant is running, or this call refused
627    /// }).expect("called from a plain thread on a root token");
628    /// # Ok::<(), threadteam::Error>(())
629    /// ```
630    pub fn broadcast<F>(self, f: F) -> Result<(), Refused>
631    where
632        F: Fn(usize) + Sync,
633    {
634        if let Some(refused) = self.refusal() {
635            return Err(refused);
636        }
637        if let Some(t) = self.team {
638            // Reserved members are exclusively this token's: every participant
639            // runs at once, also for a child produced by `fork`.
640            let _released = self.track.release();
641            let track = &self.track;
642            spread_raw(Some(t.awake), t.ids, self.budget.get(), t.over, |i| {
643                let _live = track.task();
644                f(i);
645            });
646            return Ok(());
647        }
648        let pool = match self.pool {
649            Some(pool) if self.budget.get() > 1 => pool,
650            _ => {
651                let _live = self.track.task();
652                f(0);
653                return Ok(());
654            }
655        };
656        let _released = self.track.release();
657        #[cfg(feature = "parallel")]
658        sched::broadcast(pool, &self.track, self.budget.get(), f);
659        #[cfg(not(feature = "parallel"))]
660        {
661            let _ = pool;
662            f(0);
663        }
664        Ok(())
665    }
666
667    /// Whether [`broadcast`](Exec::broadcast) would run right now, without
668    /// consuming the token.
669    ///
670    /// True exactly when `broadcast` would not return [`Refused`]: the token is
671    /// not derived from [`fork`](Exec::fork) and the calling thread is not a
672    /// worker of its pool. A serial or budget-one token returns true, because
673    /// `broadcast` then runs `f(0)` inline. The answer depends on the calling
674    /// thread, so ask it on the thread that will broadcast.
675    ///
676    /// Use it before building an expensive setup that only a broadcast needs,
677    /// instead of paying for the setup and then being refused.
678    ///
679    /// # Examples
680    ///
681    /// ```
682    /// use threadteam::{Exec, Pool};
683    ///
684    /// let pool = Pool::builder().threads(2).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
685    /// let exec = Exec::new(&pool, 2)?;
686    /// assert!(exec.can_broadcast());
687    /// exec.fork(1, |_, child| {
688    ///     let predicted = child.can_broadcast();
689    ///     assert_eq!(child.broadcast(|_| {}).is_ok(), predicted);
690    /// });
691    /// assert!(Exec::serial().can_broadcast());
692    /// # Ok::<(), threadteam::Error>(())
693    /// ```
694    pub fn can_broadcast(&self) -> bool {
695        self.refusal().is_none()
696    }
697
698    /// The single refusal rule shared by `broadcast` and `can_broadcast`.
699    fn refusal(&self) -> Option<Refused> {
700        if self.team.is_some() {
701            return None;
702        }
703        let pool = match self.pool {
704            Some(pool) if self.budget.get() > 1 => pool,
705            _ => return None,
706        };
707        if self.child {
708            return Some(Refused::ForkChild);
709        }
710        if sched::in_pool(pool) {
711            return Some(Refused::CallerInPool);
712        }
713        None
714    }
715
716    /// Hands the budget as a plain width to a library outside this stack.
717    ///
718    /// This is the only place a bare width exists. The token is consumed, the
719    /// pool is entered once (inline when the token is serial, has a budget of
720    /// one, or the caller already is a worker of the pool) and `f` receives the
721    /// budget, for example to build `faer::Par::rayon(width)` or to set a vendor
722    /// BLAS thread count. Whatever the library schedules with that width runs on
723    /// this pool, never on an ambient one. How the library divides the width is
724    /// its own responsibility. Without the `parallel` feature, `f` runs inline
725    /// and receives a width of one.
726    ///
727    /// # Placement
728    ///
729    /// The token's reserved members are **lent** for the call: each restores its
730    /// own affinity mask and serves the pool's jobs until the call returns. The
731    /// calling thread does the same when it is itself a pinned participant of
732    /// this pool (for example a member that leads a [`fork`](Exec::fork) child),
733    /// so the foreign work never keeps a team's single-CPU placement. Both
734    /// re-pin at the next pin section, that is at the next root team scope.
735    ///
736    /// # Examples
737    ///
738    /// ```
739    /// use threadteam::Exec;
740    ///
741    /// let width = Exec::serial().external(|width| width.get());
742    /// assert_eq!(width, 1);
743    /// ```
744    pub fn external<R: Send>(self, f: impl FnOnce(NonZeroUsize) -> R + Send) -> R {
745        #[cfg(feature = "parallel")]
746        if let Some(pool) = self.pool {
747            if self.budget.get() > 1 {
748                let width = self.budget;
749                let _released = self.track.release();
750                // A team token lends its reserved members to the pool for the
751                // call: the library's Rayon work runs on them as well.
752                let _lent = self.team.map(|t| LendGuard::new(t.awake, t.ids));
753                // The calling thread may itself be a pinned participant of this
754                // pool (the member that leads a `fork` child, or a token used
755                // from inside a share): it restores its own mask before it
756                // serves foreign work, as a lent member does.
757                sched::unpin_worker(pool);
758                return sched::enter(pool, &self.track, move || f(width));
759            }
760        }
761        f(NonZeroUsize::MIN)
762    }
763
764    /// Reserves `budget - 1` pool members for the scope of `f` and hands `f`
765    /// the exclusive [`TeamExec`] authority over them and the calling thread.
766    ///
767    /// The calling thread is participant 0 (the leader); the members are
768    /// pool workers that stay reserved, spinning and serving no Rayon job,
769    /// until `f` returns (or unwinds). A serial or budget-one token yields a
770    /// team of one without touching the pool.
771    ///
772    /// Acquisition never waits for a Rayon job that is already running. Idle
773    /// members of an earlier team (see [`WaitPolicy`]) are claimed with a
774    /// compare-and-swap; missing ones are requested from the pool, and the
775    /// call waits at most the pool's acquisition timeout
776    /// ([`PoolBuilder::acquire_timeout`](crate::PoolBuilder::acquire_timeout))
777    /// for idle workers to wake. If not all members are there by then, every
778    /// tentative claim is released and nothing runs. Before `f` is called,
779    /// every participant has acknowledged on its own thread.
780    ///
781    /// The token stays with the caller (this takes `&mut self`), so a refused
782    /// acquisition leaves it available for a barrier-free fallback such as
783    /// [`fork`](Exec::fork). It is conserved as usual: while `f` runs the token
784    /// is borrowed.
785    ///
786    /// This call opens a **root team scope**: it reads the calling thread's
787    /// affinity mask, which decides two things. With
788    /// [`PoolBuilder::pin_participants`](crate::PoolBuilder::pin_participants) on
789    /// it places every member on its own CPU of that mask for the scope (the
790    /// leader is never pinned).
791    /// Whether or not pinning is on, it records whether the scope is
792    /// *oversubscribed* (more participants than CPUs in the mask), which decides
793    /// how the members park and how a joining leader waits. See the design
794    /// record.
795    ///
796    /// # Errors
797    ///
798    /// [`Refused::Unavailable`] when the members could not be reserved.
799    ///
800    /// # Examples
801    ///
802    /// ```
803    /// use threadteam::{Exec, Pool, Refused};
804    ///
805    /// let pool = Pool::builder().threads(4).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
806    /// let mut exec = Exec::new(&pool, 4)?;
807    /// match exec.team(|team| team.size().get()) {
808    ///     Ok(n) => assert!(n >= 1),
809    ///     Err(Refused::Unavailable) => exec.fork(4, |_, _| {}), // barrier-free fallback
810    ///     Err(_) => unreachable!(),
811    /// }
812    /// # Ok::<(), threadteam::Error>(())
813    /// ```
814    pub fn team<R>(
815        &mut self,
816        f: impl for<'team> FnOnce(TeamExec<'team>) -> R,
817    ) -> Result<R, Refused> {
818        self.team_with(WaitOverride::new(), f)
819    }
820
821    /// [`team`](Exec::team) with a scoped override of the waiting policy.
822    ///
823    /// The override applies to this operation only and is inherited by the
824    /// children of the team; the pool's defaults are not changed. It governs
825    /// the idle interval after this reservation ends and never weakens the
826    /// reservation itself.
827    ///
828    /// # Errors
829    ///
830    /// [`Refused::Unavailable`] when the members could not be reserved.
831    pub fn team_with<R>(
832        &mut self,
833        over: WaitOverride,
834        f: impl for<'team> FnOnce(TeamExec<'team>) -> R,
835    ) -> Result<R, Refused> {
836        if let Some(t) = self.team {
837            // Carried members: the (sub)team is available without acquisition.
838            let mut team = TeamExec {
839                awake: Some(t.awake),
840                ids: t.ids,
841                policy: t.policy.with(over),
842                track: self.track.alias(),
843                over: t.over,
844            };
845            // Only the root token minted by a `Team` opens a root scope; a
846            // child, a reborrow or a sub-team re-pins nothing of its own.
847            if self._leader.is_some() {
848                // Root-scope entry, on the executing thread: the root pressure
849                // and the members' spin budget are decided by *this* thread's CPU
850                // set. A token minted on another thread (or another mask) is
851                // paced by the thread that enters the scope, exactly as its pin
852                // section is placed by that thread. Descendants inherit it.
853                let oversubscribed = crate::pin::oversubscribed(team.size().get());
854                team.over = oversubscribed;
855                if let Some(awake) = team.awake {
856                    awake.set_spin_budget(team.ids, awake.spin_budget(oversubscribed));
857                }
858                if let Some(auth) = self.team.as_mut() {
859                    auth.over = oversubscribed;
860                }
861                if team.size().get() > 1 {
862                    pin_scope(&mut team);
863                }
864            }
865            return Ok(f(team));
866        }
867        let pool = match self.pool {
868            Some(pool) if self.budget.get() > 1 => pool,
869            _ => {
870                let policy = self.pool.map_or(WaitPolicy::DEFAULT, Pool::wait_policy);
871                return Ok(f(TeamExec {
872                    awake: None,
873                    ids: &[],
874                    policy: policy.with(over),
875                    track: self.track.alias(),
876                    over: false,
877                }));
878            }
879        };
880        let policy = pool.wait_policy().with(over);
881        // Root-scope entry: the CPU set of this calling thread decides both the
882        // placement of the participants and whether the scope is
883        // oversubscribed, whatever the pool's pinning setting.
884        let oversubscribed = crate::pin::oversubscribed(self.budget.get());
885        let reservation = pool.awake.acquire(
886            pool,
887            self.budget.get() - 1,
888            policy,
889            pool.awake.spin_budget(oversubscribed),
890        )?;
891        let mut team = TeamExec {
892            awake: Some(&pool.awake),
893            ids: &reservation.ids,
894            policy,
895            track: self.track.alias(),
896            over: oversubscribed,
897        };
898        // The pin section is also the acknowledgement; without pinning the
899        // participants acknowledge as before. The leader is never pinned, so
900        // there is nothing to restore when the scope ends or unwinds.
901        let pinned = team.size().get() > 1 && pin_scope(&mut team);
902        if !pinned {
903            team.acknowledge();
904        }
905        Ok(f(team))
906    }
907
908    /// Root id of this token in the [`ledger`](crate::ledger), if it has one.
909    ///
910    /// Tokens from [`Exec::new`] are roots; children and reborrows report the
911    /// root of their origin; [`Exec::serial`] has none.
912    ///
913    /// Available with the `ledger` cargo feature.
914    ///
915    /// # Examples
916    ///
917    /// ```
918    /// use threadteam::{Exec, Pool};
919    ///
920    /// let pool = Pool::builder().threads(1).acquire_timeout(std::time::Duration::from_secs(10)).build()?;
921    /// assert!(Exec::new(&pool, 1)?.ledger_root().is_some());
922    /// assert!(Exec::serial().ledger_root().is_none());
923    /// # Ok::<(), threadteam::Error>(())
924    /// ```
925    #[cfg(feature = "ledger")]
926    pub fn ledger_root(&self) -> Option<u64> {
927        self.track.root_id()
928    }
929}
930
931/// Runs the pin section of a root team scope: every member places itself on its
932/// own CPU of the calling thread's set. The leader is never pinned.
933///
934/// `false` when member pinning is off for this pool, when the platform has no
935/// CPU set (off Linux), or when the calling thread's mask could not be read:
936/// then nothing ran and the caller acknowledges as usual.
937fn pin_scope(team: &mut TeamExec<'_>) -> bool {
938    if !team.awake.is_some_and(|awake| awake.pin) {
939        return false;
940    }
941    // The leader reads its own mask, and never changes it: that set is where
942    // the members are placed.
943    let Some(set) = crate::pin::read_here() else {
944        return false;
945    };
946    team.pin_section(&set);
947    true
948}
949
950impl fmt::Debug for Exec<'_> {
951    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
952        f.debug_struct("Exec")
953            .field("budget", &self.budget)
954            .field("pooled", &self.pool.is_some())
955            .field("child", &self.child)
956            .field("team", &self.team.is_some())
957            .finish()
958    }
959}