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}