nk 1.0.0
C++ Numerical Computing Runtime
Loading...
Searching...
No Matches
thread_pool.hpp
1// core/include/nk/ops/thread_pool.hpp
2//
3// Persistent worker pool used by SIMD kernels above their parallel
4// thresholds. The pool is built once at first use (singleton) with
5// `std::thread::hardware_concurrency()` workers and stays alive for
6// the lifetime of the process. Each `run()` call hands a function
7// `fn(start, end)` to the workers, splits [0, n) evenly, and blocks
8// until every chunk has finished.
9//
10// This header is only present when NK_WITH_THREADS is defined at
11// build time. The companion `parallel_for.hpp` includes it
12// conditionally and falls back to a sequential `fn(0, n)` otherwise,
13// so call sites stay portable across the threaded and non-threaded
14// builds.
15
16#pragma once
17
18#include <atomic>
19#include <condition_variable>
20#include <cstddef>
21#include <functional>
22#include <mutex>
23#include <thread>
24#include <vector>
25
26namespace nk::detail {
27
28class ThreadPool
29{
30public:
31 // Process-wide singleton. Sized to hardware_concurrency() at first call.
32 static ThreadPool &global();
33
34 explicit ThreadPool(int n_workers);
35 ~ThreadPool();
36
37 ThreadPool(const ThreadPool &) = delete;
38 ThreadPool &operator=(const ThreadPool &) = delete;
39
40 int workers() const { return static_cast<int>(workers_.size()); }
41
42 // Run `fn(start, end)` over an even split of [0, n) across workers,
43 // blocking until every chunk finishes. With one or zero workers this
44 // simply calls `fn(0, n)` on the calling thread.
45 //
46 // `max_workers` (when > 0) caps the number of workers actually used
47 // for this task — useful for memory-bandwidth-bound kernels where
48 // 6+ workers don't help and just add signaling noise on dual-channel
49 // DDR5. Pass 0 (the default) to use every worker.
50 void run(std::size_t n, std::function<void(std::size_t, std::size_t)> fn,
51 int max_workers = 0);
52
53private:
54 void worker_loop(int id);
55
56 std::vector<std::thread> workers_;
57 std::mutex mu_;
58 // Serializes concurrent run() submitters (e.g. two engines sharing this
59 // process-wide pool on different threads). Held across publish+wait; never
60 // contended by workers (they use mu_) nor by a nested run() (which the
61 // worker-thread guard short-circuits to an inline call before reaching it).
62 std::mutex submit_mu_;
63 std::condition_variable cv_start_;
64 std::condition_variable cv_done_;
65
66 // Current task. `epoch_` is bumped on every dispatch so workers
67 // can tell a new task apart from a spurious wake-up. `active_`
68 // is how many workers participate in this task (≤ workers_.size());
69 // workers with id ≥ active_ wake up on cv_start_ but skip the
70 // task body so they don't decrement task_remaining_.
71 std::function<void(std::size_t, std::size_t)> task_;
72 std::size_t task_n_ = 0;
73 std::atomic<int> task_remaining_{0};
74 int active_ = 0;
75 std::atomic<int> epoch_{0};
76 std::atomic<bool> shutdown_{false};
77};
78
79} // namespace nk::detail