Skip to main content

plat/
pool.rs

1// pool -- the worker thread pool: parallel loops and background jobs     [src/core/thread.s]
2//
3//   thr_init(n) -> workers          start the pool once: n workers (0 = cpu_count), at most
4//                                   THR_MAX; `--threads N` on the command line wins.  Later
5//                                   calls return the count; every function below starts it
6//   thr_count() -> workers          thr_shutdown()   stop and join the workers (no job running)
7//   parallel_for(count, &f) -> Ok / Err(Cancelled)
8//        f(index, job) for 0..count on the workers AND the calling thread; returns when every
9//        item is done.  f may call job.cancel() to skip the remaining items.  Nested loops
10//        (from inside f) are fine.
11//   Job: a background job -- set(count, f), start() (false: still running), wait(),
12//        cancel(), done(), items_done().  When it finishes the pool calls wake() (E_WAKE).
13//   thr_self() -> slot              0..n-1 on worker k, n on any other thread: per-thread
14//                                   arrays of thr_count() + 1 entries
15//
16// Design (as in ASMLIB): one queue of runnable jobs under a short spin lock (loops at the
17// head, background jobs at the tail).  A thread takes part in a job by counting itself in
18// `active` under the lock, then claims item indexes with fetch_add on `next`.  The thread
19// that sees every index handed out (or the cancel flag) unlinks the job; the last one to
20// leave finishes it: state DONE (from then on the owner may drop the record), wake() for
21// background jobs, and every thread sleeping in a job wait is woken.  Idle workers spin
22// briefly, then sleep on a semaphore; waiters spin, then sleep on a second one at most
23// WAIT_POLL_MS at a time (a semaphore post may be taken by another waiter).  Work depends
24// only on the item index, so results are the same for any number of threads.
25//
26// The loop record lives on the caller's stack and holds a pointer to the borrowed closure:
27// parallel_for returns only after the last participant left, which is what makes handing
28// the borrow to the workers sound (the unsafe below).
29
30use alloc::boxed::Box;
31use core::cell::UnsafeCell;
32use core::sync::atomic::{AtomicI32, AtomicPtr, AtomicU32, AtomicU64, AtomicUsize, Ordering::SeqCst};
33
34use crate::{Sem, Thread, cpu_count, thread_spawn, thread_yield, wake};
35
36pub const THR_MAX: usize = 64;
37const SPIN_LOCK: u32 = 64; // pause rounds before yielding on the queue lock
38const SPIN_IDLE: u32 = 256; // worker: pause rounds watching the queue before sleeping
39const SPIN_WAIT: u32 = 2000; // job waiter: pause rounds before sleeping
40const WAIT_POLL_MS: i32 = 2; // job waiter: longest sleep before it looks at the job again
41const THR_STACK: usize = 0x10_0000; // 1 MB per worker
42const JF_QUEUED: u32 = 1;
43const JF_ASYNC: u32 = 2;
44const TS_NONE: u32 = 0;
45const TS_INIT: u32 = 1;
46const TS_READY: u32 = 2;
47const JS_RUNNING: u32 = 1;
48const JS_DONE: u32 = 2;
49
50/// a loop or job ended through cancel()
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct Cancelled;
53
54type ItemFn = dyn Fn(usize, &JobCtl) + Sync;
55
56/// a job as its item functions see it: cancel the rest, look at the progress
57pub struct JobCtl {
58    f: UnsafeCell<*const ItemFn>, // fixed while the job runs
59    count: UnsafeCell<u64>,       // fixed while the job runs
60    next: AtomicU64,              // next index to hand out
61    done: AtomicU64,              // items finished
62    cancel: AtomicU32,
63    state: AtomicU32,
64    active: UnsafeCell<u32>,           // threads working on it (queue lock)
65    flags: UnsafeCell<u32>,            // JF_* (queue lock)
66    link: UnsafeCell<*const JobCtl>,   // next job in the queue (queue lock)
67}
68
69// SAFETY: the UnsafeCell fields are written under the queue lock or while no other thread
70// can see the job (not running); everything else is atomic.
71unsafe impl Sync for JobCtl {}
72unsafe impl Send for JobCtl {}
73
74impl JobCtl {
75    fn new(f: *const ItemFn, count: u64) -> JobCtl {
76        JobCtl {
77            f: UnsafeCell::new(f),
78            count: UnsafeCell::new(count),
79            next: AtomicU64::new(0),
80            done: AtomicU64::new(0),
81            cancel: AtomicU32::new(0),
82            state: AtomicU32::new(0),
83            active: UnsafeCell::new(0),
84            flags: UnsafeCell::new(0),
85            link: UnsafeCell::new(core::ptr::null()),
86        }
87    }
88
89    /// skip the items not started yet (callable from inside an item)  [thr_job_cancel]
90    pub fn cancel(&self) {
91        job_cancel(self)
92    }
93
94    /// long items poll this
95    pub fn cancelled(&self) -> bool {
96        self.cancel.load(SeqCst) != 0
97    }
98
99    pub fn count(&self) -> u64 {
100        // SAFETY: fixed while the job runs; the owner changes it only while it does not.
101        unsafe { *self.count.get() }
102    }
103
104    /// items finished so far (progress = items_done / count)
105    pub fn items_done(&self) -> u64 {
106        self.done.load(SeqCst)
107    }
108
109    fn result(&self) -> Result<(), Cancelled> {
110        if self.cancel.load(SeqCst) != 0 { Err(Cancelled) } else { Ok(()) }
111    }
112}
113
114// ---------------------------------------------------------------- the pool
115
116struct Pool {
117    lock: AtomicU32,
118    head: AtomicPtr<JobCtl>,
119    tail: AtomicPtr<JobCtl>,
120    work: UnsafeCell<Option<Sem>>,     // work was queued (written by init / shutdown only)
121    donesem: UnsafeCell<Option<Sem>>,  // a job finished (dwait sleepers)
122    dwait: AtomicI32,
123    state: AtomicU32,
124    n: AtomicU32,
125    quit: AtomicU32,
126    threads: UnsafeCell<[Option<Thread>; THR_MAX]>,
127    stk: [AtomicUsize; THR_MAX], // a stack address of each worker's first frame (thr_self)
128}
129
130// SAFETY: the UnsafeCell fields change only in thr_init / thr_shutdown, which no job may
131// overlap (as in ASMLIB); the rest is atomic.
132unsafe impl Sync for Pool {}
133
134static POOL: Pool = Pool {
135    lock: AtomicU32::new(0),
136    head: AtomicPtr::new(core::ptr::null_mut()),
137    tail: AtomicPtr::new(core::ptr::null_mut()),
138    work: UnsafeCell::new(None),
139    donesem: UnsafeCell::new(None),
140    dwait: AtomicI32::new(0),
141    state: AtomicU32::new(TS_NONE),
142    n: AtomicU32::new(0),
143    quit: AtomicU32::new(0),
144    threads: UnsafeCell::new([const { None }; THR_MAX]),
145    stk: [const { AtomicUsize::new(0) }; THR_MAX],
146};
147
148fn work_sem() -> Option<&'static Sem> {
149    // SAFETY: see Pool.
150    unsafe { (*POOL.work.get()).as_ref() }
151}
152
153fn done_sem() -> Option<&'static Sem> {
154    // SAFETY: see Pool.
155    unsafe { (*POOL.donesem.get()).as_ref() }
156}
157
158/// start the pool (once): the number of worker threads  [thr_init]
159pub fn thr_init(n: u32) -> u32 {
160    loop {
161        if POOL.state.load(SeqCst) == TS_READY {
162            return POOL.n.load(SeqCst);
163        }
164        if POOL.state.compare_exchange(TS_NONE, TS_INIT, SeqCst, SeqCst).is_ok() {
165            break;
166        }
167        thread_yield(); // another thread is starting the pool
168    }
169    let mut want = n as usize;
170    if let Some(v) = crate::arg_value("--threads").and_then(|v| v.parse::<i32>().ok()) {
171        if v > 0 {
172            want = v as usize;
173        }
174    }
175    if want == 0 {
176        want = cpu_count() as usize;
177    }
178    let want = want.min(THR_MAX);
179    POOL.quit.store(0, SeqCst);
180    POOL.head.store(core::ptr::null_mut(), SeqCst);
181    POOL.tail.store(core::ptr::null_mut(), SeqCst);
182    POOL.dwait.store(0, SeqCst);
183    let mut started = 0;
184    // SAFETY: TS_INIT: this thread alone touches the pool's cells.
185    unsafe {
186        *POOL.work.get() = Sem::new(0);
187        *POOL.donesem.get() = Sem::new(0);
188        if work_sem().is_some() && done_sem().is_some() {
189            let threads = &mut *POOL.threads.get();
190            while started < want {
191                let k = started;
192                match thread_spawn(THR_STACK, Box::new(move || worker(k))) {
193                    Some(t) => threads[k] = Some(t),
194                    None => break,
195                }
196                started += 1;
197            }
198        }
199    }
200    POOL.n.store(started as u32, SeqCst);
201    POOL.state.swap(TS_READY, SeqCst);
202    started as u32
203}
204
205/// the number of worker threads (starts the pool on first use)  [thr_count]
206pub fn thr_count() -> u32 {
207    if POOL.state.load(SeqCst) == TS_READY { POOL.n.load(SeqCst) } else { thr_init(0) }
208}
209
210/// stop the workers once the queue is empty, join them; a later call starts a new pool
211pub fn thr_shutdown() {
212    if POOL.state.load(SeqCst) != TS_READY {
213        return;
214    }
215    POOL.quit.store(1, SeqCst);
216    let n = POOL.n.load(SeqCst);
217    if let Some(s) = work_sem() {
218        s.post(n);
219    }
220    // SAFETY: no job runs (the caller's promise); the workers only read the cells.
221    unsafe {
222        let threads = &mut *POOL.threads.get();
223        for t in threads.iter_mut().take(n as usize) {
224            if let Some(t) = t.take() {
225                t.join();
226            }
227        }
228        *POOL.work.get() = None;
229        *POOL.donesem.get() = None;
230    }
231    POOL.n.store(0, SeqCst);
232    POOL.state.store(TS_NONE, SeqCst);
233}
234
235// ---------------------------------------------------------------- queue (internal)
236
237fn q_lock() {
238    loop {
239        if POOL.lock.swap(1, SeqCst) == 0 {
240            return;
241        }
242        let mut spin = SPIN_LOCK;
243        loop {
244            core::hint::spin_loop();
245            if POOL.lock.load(SeqCst) == 0 {
246                break;
247            }
248            spin -= 1;
249            if spin == 0 {
250                thread_yield();
251                break;
252            }
253        }
254    }
255}
256
257fn q_unlock() {
258    POOL.lock.store(0, SeqCst);
259}
260
261// The queue functions run with the lock held; job pointers in the queue are alive (a job
262// is unlinked before it finishes).
263unsafe fn q_push_head(j: *const JobCtl) {
264    unsafe {
265        *(*j).link.get() = POOL.head.load(SeqCst);
266        POOL.head.store(j as *mut JobCtl, SeqCst);
267        if POOL.tail.load(SeqCst).is_null() {
268            POOL.tail.store(j as *mut JobCtl, SeqCst);
269        }
270        *(*j).flags.get() |= JF_QUEUED;
271    }
272}
273
274unsafe fn q_push_tail(j: *const JobCtl) {
275    unsafe {
276        *(*j).link.get() = core::ptr::null();
277        let t = POOL.tail.load(SeqCst);
278        if t.is_null() {
279            POOL.head.store(j as *mut JobCtl, SeqCst);
280        } else {
281            *(*t).link.get() = j;
282        }
283        POOL.tail.store(j as *mut JobCtl, SeqCst);
284        *(*j).flags.get() |= JF_QUEUED;
285    }
286}
287
288unsafe fn q_unlink(j: *const JobCtl) {
289    unsafe {
290        let mut prev: *const JobCtl = core::ptr::null();
291        let mut cur = POOL.head.load(SeqCst) as *const JobCtl;
292        while !cur.is_null() {
293            if cur == j {
294                let next = *(*j).link.get();
295                if prev.is_null() {
296                    POOL.head.store(next as *mut JobCtl, SeqCst);
297                } else {
298                    *(*prev).link.get() = next;
299                }
300                if POOL.tail.load(SeqCst) as *const JobCtl == j {
301                    POOL.tail.store(prev as *mut JobCtl, SeqCst);
302                }
303                *(*j).flags.get() &= !JF_QUEUED;
304                return;
305            }
306            prev = cur;
307            cur = *(*cur).link.get();
308        }
309    }
310}
311
312/// the first queued job, now counting this thread in `active`, or null
313fn pick() -> *const JobCtl {
314    q_lock();
315    let h = POOL.head.load(SeqCst) as *const JobCtl;
316    if !h.is_null() {
317        // SAFETY: lock held; queued jobs are alive.
318        unsafe { *(*h).active.get() += 1 };
319    }
320    q_unlock();
321    h
322}
323
324/// claim and run items until none are left or the job is cancelled
325fn run_items(j: &JobCtl) {
326    // SAFETY: f is fixed and alive while the job runs (parallel_for / Job keep it).
327    let f = unsafe { &**j.f.get() };
328    let count = j.count();
329    while j.cancel.load(SeqCst) == 0 {
330        let i = j.next.fetch_add(1, SeqCst);
331        if i >= count {
332            break;
333        }
334        f(i as usize, j);
335        j.done.fetch_add(1, SeqCst);
336    }
337}
338
339/// stop taking part in a job; the last participant finishes it (a raw pointer: the
340/// owner may drop the job as soon as finish marked it DONE)
341fn leave(j: *const JobCtl) {
342    q_lock();
343    // SAFETY: lock held; this thread is still counted in `active`, so the job is alive.
344    let fin = unsafe {
345        let j = &*j;
346        if *j.flags.get() & JF_QUEUED != 0
347            && (j.cancel.load(SeqCst) != 0 || j.next.load(SeqCst) >= j.count())
348        {
349            q_unlink(j); // every index is handed out: nothing left to pick
350        }
351        *j.active.get() -= 1;
352        *j.active.get() == 0 && *j.flags.get() & JF_QUEUED == 0
353    };
354    q_unlock();
355    if fin {
356        finish(j);
357    }
358}
359
360/// DONE (the owner may drop the record from then on), wake() for background jobs, wake
361/// every thread sleeping in wait_done
362fn finish(j: *const JobCtl) {
363    // SAFETY: the job is alive until its state is DONE; flags are read before that.
364    unsafe {
365        let flags = *(*j).flags.get();
366        (*j).state.swap(JS_DONE, SeqCst);
367        // (j must not be touched from here on)
368        finish_rest(flags);
369    }
370}
371
372fn finish_rest(flags: u32) {
373    if flags & JF_ASYNC != 0 {
374        wake();
375    }
376    let d = POOL.dwait.swap(0, SeqCst);
377    if d > 0 {
378        if let Some(s) = done_sem() {
379            s.post(d as u32);
380        }
381    }
382}
383
384/// block until the job is DONE: a short spin, then the done semaphore WAIT_POLL_MS at a time
385fn wait_done(j: &JobCtl) {
386    for _ in 0..SPIN_WAIT {
387        if j.state.load(SeqCst) == JS_DONE {
388            return;
389        }
390        core::hint::spin_loop();
391    }
392    loop {
393        // register first, then check: finish() sets the state before it reads the count
394        POOL.dwait.fetch_add(1, SeqCst);
395        if j.state.load(SeqCst) == JS_DONE {
396            return;
397        }
398        match done_sem() {
399            Some(s) => {
400                if !s.wait(WAIT_POLL_MS) {
401                    // timed out: take the registration back (unless finish made it a post)
402                    let mut v = POOL.dwait.load(SeqCst);
403                    while v > 0 {
404                        match POOL.dwait.compare_exchange(v, v - 1, SeqCst, SeqCst) {
405                            Ok(_) => break,
406                            Err(x) => v = x,
407                        }
408                    }
409                }
410            }
411            None => thread_yield(),
412        }
413        if j.state.load(SeqCst) == JS_DONE {
414            return;
415        }
416    }
417}
418
419fn worker(k: usize) {
420    let top = 0u8;
421    POOL.stk[k].store(core::hint::black_box(&top) as *const u8 as usize, SeqCst);
422    'outer: loop {
423        let j = pick();
424        if !j.is_null() {
425            // SAFETY: pick counted this thread in `active`: the job stays alive until leave.
426            let j = unsafe { &*j };
427            run_items(j);
428            leave(j);
429            continue;
430        }
431        if POOL.quit.load(SeqCst) != 0 {
432            break;
433        }
434        for _ in 0..SPIN_IDLE {
435            core::hint::spin_loop();
436            if !POOL.head.load(SeqCst).is_null() {
437                continue 'outer;
438            }
439        }
440        if let Some(s) = work_sem() {
441            s.wait(-1);
442        }
443    }
444    POOL.stk[k].store(0, SeqCst);
445}
446
447// ---------------------------------------------------------------- loops and jobs
448
449/// f(index, job) for every index in 0..count, on the workers and this thread  [thr_parallel_for]
450pub fn parallel_for(count: usize, f: &(dyn Fn(usize, &JobCtl) + Sync)) -> Result<(), Cancelled> {
451    if count == 0 {
452        return Ok(());
453    }
454    // a plugin's loop (policy::on_behalf): its items keep the plugin's rights on every worker
455    let scopes = crate::policy::scopes_here();
456    if !scopes.is_empty() {
457        let s = &scopes;
458        return loop_items(count, &|i, j| crate::policy::in_scopes(s, || f(i, j)));
459    }
460    loop_items(count, f)
461}
462
463fn loop_items(count: usize, f: &(dyn Fn(usize, &JobCtl) + Sync)) -> Result<(), Cancelled> {
464    // SAFETY: the lifetime of f is erased for the record; the workers use it only until the
465    // last of them left the job, and this function returns only after that (wait_done).
466    let fp: *const ItemFn = unsafe { core::mem::transmute::<&(dyn Fn(usize, &JobCtl) + Sync), *const ItemFn>(f) };
467    let job = JobCtl::new(fp, count as u64);
468    job.state.store(JS_RUNNING, SeqCst);
469    // SAFETY: not shared yet.
470    unsafe { *job.active.get() = 1 }; // the calling thread
471    let n = thr_count();
472    if n == 0 || count <= 1 {
473        run_items(&job);
474        job.state.store(JS_DONE, SeqCst);
475    } else {
476        q_lock();
477        // SAFETY: lock held; job outlives its time in the queue (see above).
478        unsafe { q_push_head(&job) }; // the caller waits: before background jobs
479        q_unlock();
480        if let Some(s) = work_sem() {
481            s.post((count - 1).min(n as usize) as u32);
482        }
483        run_items(&job);
484        leave(&job);
485        wait_done(&job);
486    }
487    job.result()
488}
489
490/// cancel: items not started are skipped; a queued job nobody works on finishes at once
491/// (callers keep j alive: its owner, or an item while it runs)
492fn job_cancel(j: &JobCtl) {
493    j.cancel.swap(1, SeqCst);
494    if j.state.load(SeqCst) != JS_RUNNING {
495        return;
496    }
497    q_lock();
498    // SAFETY: lock held.
499    let fin = unsafe {
500        if *j.flags.get() & JF_QUEUED != 0 && *j.active.get() == 0 {
501            q_unlink(j);
502            true
503        } else {
504            false
505        }
506    };
507    q_unlock();
508    if fin {
509        finish(j);
510    }
511}
512
513/// a background job: count items of f on the workers; wake() when it is finished
514pub struct Job {
515    rec: Box<JobCtl>,
516    f: Option<Box<dyn Fn(usize, &JobCtl) + Send + Sync>>,
517}
518
519impl Default for Job {
520    fn default() -> Self {
521        Self::new()
522    }
523}
524
525impl Job {
526    pub fn new() -> Job {
527        Job { rec: Box::new(JobCtl::new(core::ptr::null::<fn(usize, &JobCtl)>() as *const ItemFn, 0)), f: None }
528    }
529
530    fn running(&self) -> bool {
531        self.rec.state.load(SeqCst) == JS_RUNNING
532    }
533
534    /// the work of the next start: count items of f (false while the job runs)
535    pub fn set(&mut self, count: u64, f: impl Fn(usize, &JobCtl) + Send + Sync + 'static) -> bool {
536        if self.running() {
537            return false;
538        }
539        let b: Box<dyn Fn(usize, &JobCtl) + Send + Sync> = Box::new(f);
540        // SAFETY: not running: no worker reads the cells; the box lives in self.f.
541        unsafe {
542            *self.rec.f.get() = &*b as &(dyn Fn(usize, &JobCtl) + Sync) as *const ItemFn;
543            *self.rec.count.get() = count;
544        }
545        self.f = Some(b);
546        true
547    }
548
549    /// another item count for the same function (false while the job runs)
550    pub fn set_count(&mut self, count: u64) -> bool {
551        if self.running() {
552            return false;
553        }
554        // SAFETY: not running.
555        unsafe { *self.rec.count.get() = count };
556        true
557    }
558
559    /// run it in the background; false: it is still running  [thr_job_start]
560    pub fn start(&self) -> bool {
561        let j: &JobCtl = &self.rec;
562        if self.running() {
563            return false;
564        }
565        let n = thr_count();
566        j.next.store(0, SeqCst);
567        j.done.store(0, SeqCst);
568        j.cancel.store(0, SeqCst);
569        // SAFETY: not running: nobody else sees the cells.
570        unsafe {
571            *j.link.get() = core::ptr::null();
572            *j.active.get() = 0;
573            *j.flags.get() = JF_ASYNC;
574        }
575        j.state.store(JS_RUNNING, SeqCst);
576        if j.count() == 0 || self.f.is_none() {
577            finish(j);
578            return true;
579        }
580        if n == 0 {
581            // SAFETY: not shared.
582            unsafe { *j.active.get() = 1 }; // no workers: run it right here
583            run_items(j);
584            finish(j);
585            return true;
586        }
587        q_lock();
588        // SAFETY: lock held; the record lives in a Box that Drop keeps until the job is done.
589        unsafe { q_push_tail(j) };
590        q_unlock();
591        if let Some(s) = work_sem() {
592            s.post(j.count().min(n as u64) as u32);
593        }
594        true
595    }
596
597    /// block until the job is finished (this thread helps with items not taken yet)
598    pub fn wait(&self) -> Result<(), Cancelled> {
599        let j: &JobCtl = &self.rec;
600        if self.running() {
601            q_lock();
602            // SAFETY: lock held.
603            let help = unsafe {
604                if *j.flags.get() & JF_QUEUED != 0 {
605                    *j.active.get() += 1;
606                    true
607                } else {
608                    false
609                }
610            };
611            q_unlock();
612            if help {
613                run_items(j);
614                leave(j);
615            }
616            wait_done(j);
617        }
618        j.result()
619    }
620
621    /// skip the items not started yet; the job still finishes (wake)
622    pub fn cancel(&self) {
623        job_cancel(&self.rec)
624    }
625
626    /// finished (or never started)
627    pub fn done(&self) -> bool {
628        !self.running()
629    }
630
631    pub fn items_done(&self) -> u64 {
632        self.rec.items_done()
633    }
634
635    pub fn count(&self) -> u64 {
636        self.rec.count()
637    }
638}
639
640impl Drop for Job {
641    fn drop(&mut self) {
642        if self.running() {
643            self.cancel();
644            let _ = self.wait();
645        }
646    }
647}
648
649/// the calling thread's slot: k on worker k, the worker count on any other thread  [thr_self]
650pub fn thr_self() -> usize {
651    let here = 0u8;
652    let sp = core::hint::black_box(&here) as *const u8 as usize;
653    let n = (POOL.n.load(SeqCst) as usize).min(THR_MAX);
654    for k in 0..n {
655        let top = POOL.stk[k].load(SeqCst);
656        if top != 0 && sp <= top && sp > top.wrapping_sub(THR_STACK - 65536) {
657            return k;
658        }
659    }
660    n
661}