Skip to main content

strided_exec/
exec.rs

1//! The splittable placement token.
2
3use crate::error::{Error, Refused};
4use crate::pool::Pool;
5use crate::sched;
6use crate::split;
7use crate::track::Track;
8use core::fmt;
9use core::num::NonZeroUsize;
10use core::ops::Range;
11use core::sync::atomic::{AtomicUsize, Ordering};
12
13/// `budget` slots of a [`Pool`], or the calling thread alone.
14///
15/// An `Exec` is the only way kernels start parallel work. It is a *token*:
16///
17/// * It is neither `Clone` nor `Copy`, and every parallel entry consumes it, so
18///   a kernel cannot run two phases at full width at the same time. Use
19///   [`reborrow`](Exec::reborrow) to run several phases in sequence.
20/// * Only [`Exec::new`] mints a token, and it needs the `&Pool`; a token never
21///   hands its pool back out, so a kernel cannot mint a fresh full-budget token.
22/// * [`fork`](Exec::fork) divides the budget among its tasks (shares differ by
23///   at most one and sum to the budget), so the number of tasks running at once
24///   under one root token is at most its budget at every nesting depth.
25///
26/// A parent waiting for its children keeps no slot.
27///
28/// # Placement
29///
30/// Serial, budget one, or one task: the work runs inline on the calling thread
31/// and the scheduler is never called, so no global pool is started. The caller
32/// is a worker of the pool: the tasks run as a join tree on the caller, nothing is
33/// installed. The caller is a plain thread or a worker of a different pool: the
34/// pool is entered once and the caller waits; the tasks run on the target pool.
35/// No thread-local state is involved at any point.
36///
37/// # Examples
38///
39/// ```
40/// use std::sync::atomic::{AtomicUsize, Ordering};
41/// use strided_exec::{Exec, Pool};
42///
43/// let pool = Pool::builder().threads(2).build()?;
44/// let exec = Exec::new(&pool, 2)?;
45/// let sum = AtomicUsize::new(0);
46/// exec.for_ranges(100, 1.try_into().unwrap(), |r| {
47///     sum.fetch_add(r.sum::<usize>(), Ordering::Relaxed);
48/// });
49/// assert_eq!(sum.into_inner(), 4950);
50/// # Ok::<(), strided_exec::Error>(())
51/// ```
52///
53/// A token cannot be duplicated:
54///
55/// ```compile_fail
56/// fn needs_clone<T: Clone>() {}
57/// needs_clone::<strided_exec::Exec<'static>>();
58/// ```
59///
60/// ```compile_fail
61/// fn needs_copy<T: Copy>() {}
62/// needs_copy::<strided_exec::Exec<'static>>();
63/// ```
64///
65/// ```compile_fail
66/// let exec = strided_exec::Exec::serial();
67/// exec.fork(1, |_, _| {});
68/// exec.fork(1, |_, _| {}); // `fork` consumed the token
69/// ```
70#[must_use = "an Exec is a budget of execution slots; pass it to a kernel"]
71pub struct Exec<'p> {
72    pool: Option<&'p Pool>,
73    budget: NonZeroUsize,
74    /// Produced by `fork` (directly or through `reborrow`).
75    child: bool,
76    track: Track,
77}
78
79impl Exec<'static> {
80    /// A token that runs everything inline on the calling thread.
81    ///
82    /// It never touches a scheduler, so using it cannot start any pool.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// use strided_exec::Exec;
88    ///
89    /// let exec = Exec::serial();
90    /// assert!(exec.is_serial());
91    /// let here = std::thread::current().id();
92    /// exec.fork(3, |_, _| assert_eq!(std::thread::current().id(), here));
93    /// ```
94    pub const fn serial() -> Exec<'static> {
95        Exec {
96            pool: None,
97            budget: NonZeroUsize::MIN,
98            child: false,
99            track: Track::NONE,
100        }
101    }
102}
103
104impl<'p> Exec<'p> {
105    /// A token of `budget` slots of `pool`, clamped to the pool's width.
106    ///
107    /// This is the only place a budget is clamped.
108    ///
109    /// # Errors
110    ///
111    /// [`Error::ZeroBudget`] when `budget` is zero.
112    ///
113    /// # Examples
114    ///
115    /// ```
116    /// use strided_exec::{Error, Exec, Pool};
117    ///
118    /// let pool = Pool::builder().threads(2).build()?;
119    /// let exec = Exec::new(&pool, 64)?;
120    /// assert_eq!(exec.budget(), pool.width());
121    /// assert_eq!(Exec::new(&pool, 0).unwrap_err(), Error::ZeroBudget);
122    /// # Ok::<(), Error>(())
123    /// ```
124    pub fn new(pool: &'p Pool, budget: usize) -> Result<Exec<'p>, Error> {
125        let budget = NonZeroUsize::new(budget).ok_or(Error::ZeroBudget)?;
126        Ok(Exec {
127            pool: Some(pool),
128            budget: budget.min(pool.width()),
129            child: false,
130            track: Track::root(),
131        })
132    }
133
134    /// The number of slots this token stands for.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// assert_eq!(strided_exec::Exec::serial().budget().get(), 1);
140    /// ```
141    pub fn budget(&self) -> NonZeroUsize {
142        self.budget
143    }
144
145    /// Whether no parallelism is possible: the budget is one slot.
146    ///
147    /// Kernels use this to take their serial path directly.
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// assert!(strided_exec::Exec::serial().is_serial());
153    /// ```
154    pub fn is_serial(&self) -> bool {
155        self.budget.get() == 1
156    }
157
158    /// Narrows the token to at most `max` slots.
159    ///
160    /// The result has budget `min(budget, max)` and the same pool; the dropped
161    /// slots are simply not used, so slots are conserved. Callers narrow before
162    /// [`broadcast`](Exec::broadcast) or [`external`](Exec::external) to match a team size chosen
163    /// by their own work model. Co-scheduling eligibility is unchanged by
164    /// `limit`: a root token stays eligible for `broadcast`, a child produced by
165    /// [`fork`](Exec::fork) stays ineligible.
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use std::num::NonZeroUsize;
171    /// use strided_exec::{Exec, Pool};
172    ///
173    /// let pool = Pool::builder().threads(4).build()?;
174    /// let exec = Exec::new(&pool, 4)?.limit(NonZeroUsize::new(2).unwrap());
175    /// assert!(exec.budget().get() <= 2);
176    /// # Ok::<(), strided_exec::Error>(())
177    /// ```
178    pub fn limit(self, max: NonZeroUsize) -> Exec<'p> {
179        Exec {
180            budget: self.budget.min(max),
181            ..self
182        }
183    }
184
185    /// A token with the same pool and budget, borrowed from `self`.
186    ///
187    /// Parallel entries consume the token, but a kernel often runs several
188    /// parallel phases in sequence. Each phase takes a reborrow. The parent is
189    /// exclusively borrowed and unusable while the child lives, so the slots are
190    /// held by one token at a time: this conserves slots exactly as handing the
191    /// token over would.
192    ///
193    /// # Examples
194    ///
195    /// ```
196    /// use strided_exec::Exec;
197    ///
198    /// let mut exec = Exec::serial();
199    /// exec.reborrow().fork(2, |_, _| {});
200    /// exec.reborrow().fork(2, |_, _| {});
201    /// exec.fork(2, |_, _| {});
202    /// ```
203    pub fn reborrow(&mut self) -> Exec<'_> {
204        Exec {
205            pool: self.pool,
206            budget: self.budget,
207            child: self.child,
208            track: self.track.alias(),
209        }
210    }
211
212    /// Runs `jobs` jobs on `min(jobs, budget)` tasks; job `i` gets a child token.
213    ///
214    /// The budget is split into one share per task, shares differing by at most
215    /// one and summing to the budget. A job's child token carries its task's
216    /// share and may be passed to any kernel, which can `fork` again inside it.
217    /// When `jobs` exceeds the budget the tasks take job indices from a shared
218    /// counter, so the load balances; otherwise job `i` runs on task `i`.
219    ///
220    /// One task, a serial token, or `jobs <= 1` runs inline on the calling
221    /// thread without any scheduler call. Returns when every job has finished.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// use std::sync::atomic::{AtomicUsize, Ordering};
227    /// use strided_exec::{Exec, Pool};
228    ///
229    /// let pool = Pool::builder().threads(2).build()?;
230    /// let hits: Vec<AtomicUsize> = (0..5).map(|_| AtomicUsize::new(0)).collect();
231    /// Exec::new(&pool, 2)?.fork(5, |i, _child| {
232    ///     hits[i].fetch_add(1, Ordering::Relaxed);
233    /// });
234    /// assert!(hits.iter().all(|h| h.load(Ordering::Relaxed) == 1));
235    /// # Ok::<(), strided_exec::Error>(())
236    /// ```
237    pub fn fork<F>(self, jobs: usize, f: F)
238    where
239        F: Fn(usize, Exec<'_>) + Sync,
240    {
241        if jobs == 0 {
242            return;
243        }
244        let budget = self.budget.get();
245        let tasks = jobs.min(budget);
246        let _released = self.track.release();
247        let next = AtomicUsize::new(0);
248        let this = &self;
249        let body = |k: usize| {
250            let _live = this.track.task();
251            let share = split::share(budget, tasks, k);
252            let child = || Exec {
253                pool: this.pool,
254                // `share >= 1` because `tasks <= budget`.
255                budget: NonZeroUsize::new(share).unwrap_or(NonZeroUsize::MIN),
256                child: true,
257                track: this.track.child(),
258            };
259            if jobs <= tasks {
260                f(k, child());
261            } else {
262                loop {
263                    let job = next.fetch_add(1, Ordering::Relaxed);
264                    if job >= jobs {
265                        break;
266                    }
267                    f(job, child());
268                }
269            }
270        };
271        match self.pool {
272            Some(pool) if tasks > 1 => sched::run_tasks(pool, tasks, &self.track, body),
273            _ => {
274                for k in 0..tasks {
275                    body(k);
276                }
277            }
278        }
279    }
280
281    /// Data-parallel loop over `0..len` in contiguous ranges.
282    ///
283    /// Uses at most `budget` tasks; every range is at least `grain` long except
284    /// possibly the last, and together the ranges cover `0..len` exactly once.
285    /// When one task suffices (`len <= grain`, a serial token) `f(0..len)` runs
286    /// inline on the calling thread without any scheduler call. `len == 0` does
287    /// nothing.
288    ///
289    /// # Examples
290    ///
291    /// ```
292    /// use std::sync::atomic::{AtomicUsize, Ordering};
293    /// use strided_exec::{Exec, Pool};
294    ///
295    /// let pool = Pool::builder().threads(2).build()?;
296    /// let covered = AtomicUsize::new(0);
297    /// Exec::new(&pool, 2)?.for_ranges(10, 4.try_into().unwrap(), |r| {
298    ///     covered.fetch_add(r.len(), Ordering::Relaxed);
299    /// });
300    /// assert_eq!(covered.into_inner(), 10);
301    /// # Ok::<(), strided_exec::Error>(())
302    /// ```
303    pub fn for_ranges<F>(self, len: usize, grain: NonZeroUsize, f: F)
304    where
305        F: Fn(Range<usize>) + Sync,
306    {
307        if len == 0 {
308            return;
309        }
310        let (chunk, tasks) = split::range_plan(len, grain.get(), self.budget.get());
311        let _released = self.track.release();
312        let track = &self.track;
313        let body = |k: usize| {
314            let _live = track.task();
315            f(split::chunk_range(len, chunk, k));
316        };
317        match self.pool {
318            Some(pool) if tasks > 1 => sched::run_tasks(pool, tasks, track, body),
319            _ => {
320                for k in 0..tasks {
321                    body(k);
322                }
323            }
324        }
325    }
326
327    /// Co-scheduling for barrier-bearing kernels: runs `f(i)` for every
328    /// `i in 0..budget` with all participants running concurrently, or refuses.
329    ///
330    /// A serial or budget-one token runs `f(0)` inline and returns `Ok`.
331    /// Otherwise the pool is entered once and every participant runs on its own
332    /// worker, so `f` may block on a barrier shared by the `budget`
333    /// participants.
334    ///
335    /// # Errors
336    ///
337    /// Nothing runs and [`Refused`] is returned when co-scheduling cannot be
338    /// guaranteed:
339    ///
340    /// * [`Refused::ForkChild`]: the token was produced by [`fork`](Exec::fork)
341    ///   (or derived from such a token by `reborrow` or `limit`); its slots may
342    ///   share workers with siblings. Roots from [`Exec::new`], and their
343    ///   `reborrow`s and `limit`s, stay eligible.
344    ///
345    /// Callers must handle `Refused` by falling back to a barrier-free path.
346    /// * [`Refused::CallerInPool`]: the calling thread is a worker of the
347    ///   token's pool and would occupy a slot.
348    ///
349    /// # Guarantee and limits
350    ///
351    /// Concurrent broadcasts on one pool are queued on every worker in the same
352    /// order, so two of them cannot each hold half the workers. The participants
353    /// are the pool's workers `0..budget`; if unrelated tasks that never finish
354    /// occupy those workers, the broadcast waits for them. Keep barrier kernels on a pool whose workers are not held
355    /// by blocking work of another token.
356    ///
357    /// # Examples
358    ///
359    /// ```
360    /// use std::sync::Barrier;
361    /// use strided_exec::{Exec, Pool};
362    ///
363    /// let pool = Pool::builder().threads(2).build()?;
364    /// let exec = Exec::new(&pool, 2)?;
365    /// let barrier = Barrier::new(exec.budget().get());
366    /// exec.broadcast(|_i| {
367    ///     barrier.wait(); // every participant is running, or this call refused
368    /// }).expect("called from a plain thread on a root token");
369    /// # Ok::<(), strided_exec::Error>(())
370    /// ```
371    pub fn broadcast<F>(self, f: F) -> Result<(), Refused>
372    where
373        F: Fn(usize) + Sync,
374    {
375        if let Some(refused) = self.refusal() {
376            return Err(refused);
377        }
378        let pool = match self.pool {
379            Some(pool) if self.budget.get() > 1 => pool,
380            _ => {
381                let _live = self.track.task();
382                f(0);
383                return Ok(());
384            }
385        };
386        let _released = self.track.release();
387        #[cfg(feature = "parallel")]
388        sched::broadcast(pool, &self.track, self.budget.get(), f);
389        #[cfg(not(feature = "parallel"))]
390        {
391            let _ = pool;
392            f(0);
393        }
394        Ok(())
395    }
396
397    /// Whether [`broadcast`](Exec::broadcast) would run right now, without
398    /// consuming the token.
399    ///
400    /// True exactly when `broadcast` would not return [`Refused`]: the token is
401    /// not derived from [`fork`](Exec::fork) and the calling thread is not a
402    /// worker of its pool. A serial or budget-one token returns true, because
403    /// `broadcast` then runs `f(0)` inline. The answer depends on the calling
404    /// thread, so ask it on the thread that will broadcast.
405    ///
406    /// Use it before building an expensive setup that only a broadcast needs,
407    /// instead of paying for the setup and then being refused.
408    ///
409    /// # Examples
410    ///
411    /// ```
412    /// use strided_exec::{Exec, Pool};
413    ///
414    /// let pool = Pool::builder().threads(2).build()?;
415    /// let exec = Exec::new(&pool, 2)?;
416    /// assert!(exec.can_broadcast());
417    /// exec.fork(1, |_, child| {
418    ///     let predicted = child.can_broadcast();
419    ///     assert_eq!(child.broadcast(|_| {}).is_ok(), predicted);
420    /// });
421    /// assert!(Exec::serial().can_broadcast());
422    /// # Ok::<(), strided_exec::Error>(())
423    /// ```
424    pub fn can_broadcast(&self) -> bool {
425        self.refusal().is_none()
426    }
427
428    /// The single refusal rule shared by `broadcast` and `can_broadcast`.
429    fn refusal(&self) -> Option<Refused> {
430        let pool = match self.pool {
431            Some(pool) if self.budget.get() > 1 => pool,
432            _ => return None,
433        };
434        if self.child {
435            return Some(Refused::ForkChild);
436        }
437        if sched::in_pool(pool) {
438            return Some(Refused::CallerInPool);
439        }
440        None
441    }
442
443    /// Hands the budget as a plain width to a library outside this stack.
444    ///
445    /// This is the only place a bare width exists. The token is consumed, the
446    /// pool is entered once (inline when the token is serial, has a budget of
447    /// one, or the caller already is a worker of the pool) and `f` receives the
448    /// budget, for example to build `faer::Par::rayon(width)` or to set a vendor
449    /// BLAS thread count. Whatever the library schedules with that width runs on
450    /// this pool, never on an ambient one. How the library divides the width is
451    /// its own responsibility. Without the `parallel` feature, `f` runs inline
452    /// and receives a width of one.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use strided_exec::Exec;
458    ///
459    /// let width = Exec::serial().external(|width| width.get());
460    /// assert_eq!(width, 1);
461    /// ```
462    pub fn external<R: Send>(self, f: impl FnOnce(NonZeroUsize) -> R + Send) -> R {
463        #[cfg(feature = "parallel")]
464        if let Some(pool) = self.pool {
465            if self.budget.get() > 1 {
466                let width = self.budget;
467                let _released = self.track.release();
468                return sched::enter(pool, &self.track, move || f(width));
469            }
470        }
471        f(NonZeroUsize::MIN)
472    }
473
474    /// Root id of this token in the [`ledger`](crate::ledger), if it has one.
475    ///
476    /// Tokens from [`Exec::new`] are roots; children and reborrows report the
477    /// root of their origin; [`Exec::serial`] has none.
478    ///
479    /// Available with the `ledger` cargo feature.
480    ///
481    /// # Examples
482    ///
483    /// ```
484    /// use strided_exec::{Exec, Pool};
485    ///
486    /// let pool = Pool::builder().threads(1).build()?;
487    /// assert!(Exec::new(&pool, 1)?.ledger_root().is_some());
488    /// assert!(Exec::serial().ledger_root().is_none());
489    /// # Ok::<(), strided_exec::Error>(())
490    /// ```
491    #[cfg(feature = "ledger")]
492    pub fn ledger_root(&self) -> Option<u64> {
493        self.track.root_id()
494    }
495}
496
497impl fmt::Debug for Exec<'_> {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        f.debug_struct("Exec")
500            .field("budget", &self.budget)
501            .field("pooled", &self.pool.is_some())
502            .field("child", &self.child)
503            .finish()
504    }
505}