dispenso 1.6.2
A library for task parallelism
Loading...
Searching...
No Matches
thread_pool.h
Go to the documentation of this file.
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
15#pragma once
16
17#include <atomic>
18#include <cassert>
19#include <condition_variable>
20#include <cstdlib>
21#include <deque>
22#include <iterator>
23#include <mutex>
24#include <thread>
25
26#include <moodycamel/concurrentqueue.h>
27
29#include <dispenso/cpu_set.h>
30#include <dispenso/detail/math.h>
31#include <dispenso/detail/per_thread_info.h>
32#include <dispenso/detail/thread_pool_wake.h>
35#include <dispenso/platform.h>
37
38namespace dispenso {
39
40namespace detail {
41// Relaxed atomic load with TSAN happens-after annotation.
42// Semantically equivalent to memory_order_consume (which compilers promote to
43// acquire). On real hardware, address-dependent loads are naturally ordered —
44// you can't dereference a pointer before loading it. The relaxed load avoids
45// the acquire fence cost on weakly-ordered architectures (ARM: ldr vs ldar).
46// The TSAN annotation establishes the happens-before edge that the C++ abstract
47// machine requires but hardware provides for free via dependency ordering.
48template <typename T>
49T* consumeLoad(std::atomic<T*>& ptr) {
50 T* p = ptr.load(std::memory_order_relaxed);
51 DISPENSO_TSAN_ANNOTATE_HAPPENS_AFTER(&ptr);
52 return p;
53}
54} // namespace detail
55
56namespace detail {
57template <typename Result>
58class FutureBase;
59template <typename Result>
60class FutureImplBase;
61} // namespace detail
62
63#if !defined(DISPENSO_WAKEUP_ENABLE)
64#if defined(_WIN32) || defined(__linux__) || defined(__MACH__) || defined(__FreeBSD__)
65#define DISPENSO_WAKEUP_ENABLE 1
66#else
67#define DISPENSO_WAKEUP_ENABLE 0
68#endif // platform
69#endif // DISPENSO_WAKEUP_ENABLE
70
71// Poll-mode timeout: the interval at which idle threads wake to check for work
72// when explicit wake signaling is disabled. Short because polling is the only
73// mechanism that discovers newly scheduled work.
74#if !defined(DISPENSO_POLL_PERIOD_US)
75#if defined(_WIN32)
76#define DISPENSO_POLL_PERIOD_US 1000
77#else
78#define DISPENSO_POLL_PERIOD_US 200
79#endif
80#endif // DISPENSO_POLL_PERIOD_US
81
82// Wake-mode backstop: in wake mode, threads are woken explicitly via
83// futex/WaitOnAddress/ulock. This timeout bounds worst-case latency from rare
84// races (e.g., a thread entering sleep between wakeAll's epoch bump and its
85// own enterSleep). Uniform across all platforms — the wake system handles
86// normal-path latency; this is purely a safety net.
87#if !defined(DISPENSO_WAKE_BACKSTOP_US)
88#define DISPENSO_WAKE_BACKSTOP_US 100000
89#endif
90
91constexpr bool kDefaultWakeupEnable = DISPENSO_WAKEUP_ENABLE;
92
93constexpr uint32_t kDefaultSleepLenUs =
94 kDefaultWakeupEnable ? DISPENSO_WAKE_BACKSTOP_US : DISPENSO_POLL_PERIOD_US;
95
101struct ForceQueuingTag {};
102
108class DISPENSO_CACHELINE_ALIGNED ThreadPool {
109 public:
117 DISPENSO_DLL_ACCESS ThreadPool(size_t n, size_t poolLoadMultiplier = 32);
118
137 template <class Rep, class Period>
139 bool enable,
140 const std::chrono::duration<Rep, Period>& sleepDuration =
141 std::chrono::microseconds(kDefaultSleepLenUs)) {
142 setSignalingWake(
143 enable,
144 static_cast<uint32_t>(
145 std::chrono::duration_cast<std::chrono::microseconds>(sleepDuration).count()));
146 }
147
154 DISPENSO_DLL_ACCESS void resize(ssize_t n) DISPENSO_NO_THREAD_SAFETY_ANALYSIS {
155 std::lock_guard<std::mutex> lk(threadsMutex_);
156 resizeLocked(n);
157 }
158
165 ssize_t numThreads() const {
166 return numThreads_.load(std::memory_order_relaxed);
167 }
168
177 template <typename F>
178 DISPENSO_REQUIRES(OnceCallableFunc<F>)
179 void schedule(F&& f);
180
189 template <typename F>
190 DISPENSO_REQUIRES(OnceCallableFunc<F>)
191 void schedule(F&& f, ForceQueuingTag);
192
205 template <typename Generator>
206 void scheduleBulk(size_t count, Generator&& gen);
207
213 DISPENSO_DLL_ACCESS ~ThreadPool();
214
215 private:
216 class PerThreadData {
217 public:
218 void setThread(std::thread&& t);
219
220 bool running();
221
222 void stop();
223
224 ~PerThreadData();
225
226 alignas(kCacheLineSize) std::thread thread_;
227 std::atomic<bool> running_{true};
228 };
229
230 DISPENSO_DLL_ACCESS uint32_t waitOnThread(int32_t threadIdx, uint32_t priorEpoch);
231
232 void setSignalingWake(bool enable, uint32_t sleepDurationUs) DISPENSO_NO_THREAD_SAFETY_ANALYSIS {
233 std::lock_guard<std::mutex> lk(threadsMutex_);
234 ssize_t currentPoolSize = numThreads();
235 resizeLocked(0);
236 enableEpochWaiter_.store(enable, std::memory_order_release);
237 sleepLengthUs_.store(sleepDurationUs, std::memory_order_release);
238 resizeLocked(currentPoolSize);
239 }
240
241 DISPENSO_DLL_ACCESS void resizeLocked(ssize_t n);
242
243 void executeNext(OnceFunction work);
244
245 template <bool kUseWakeSleep>
246 void threadLoopImpl(PerThreadData& threadData, int32_t ringIndex);
247
248 void threadLoopWake(PerThreadData& threadData, int32_t ringIndex) {
249 threadLoopImpl<true>(threadData, ringIndex);
250 }
251 void threadLoopPoll(PerThreadData& threadData, int32_t ringIndex) {
252 threadLoopImpl<false>(threadData, ringIndex);
253 }
254
255 void markWorkDone(bool& isWorking);
256 void markIdle(bool& isWorking);
257
258 bool tryExecuteNext();
259 bool tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token);
260 bool tryExecuteNextFromRings(size_t& startRing);
261
262 // Load-factor check shared by all inline-or-queue schedule overloads.
263 DISPENSO_INLINE bool shouldRunInline();
264
265 // Core scheduling: central queue + bulk-like wake (default, throughput-oriented).
266 DISPENSO_INLINE void scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token);
267
268 // Placed scheduling: proactive wake → steal ring → central queue.
269 // Higher per-call cost but better latency for individual tasks (futures, pipelines).
270 DISPENSO_INLINE void scheduleImplPlaced(OnceFunction task, moodycamel::ProducerToken* token);
271
272 // Shared body for all ForceQueuingTag overloads. kPlaced selects
273 // scheduleImplPlaced (true) vs scheduleImpl (false).
274 template <bool kPlaced, typename F>
275 inline void forceEnqueue(F&& f, moodycamel::ProducerToken* token);
276
277 template <typename F>
278 void schedule(moodycamel::ProducerToken& token, F&& f);
279
280 template <typename F>
281 void schedule(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag);
282
283 template <typename F>
284 void schedulePlaced(moodycamel::ProducerToken& token, F&& f);
285
286 template <typename F>
287 void schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag);
288
289 // Placed scheduling: public-like API but private — only for internal dispenso callers.
290 template <typename F>
291 DISPENSO_REQUIRES(OnceCallableFunc<F>)
292 void schedulePlaced(F&& f);
293
294 template <typename F>
295 DISPENSO_REQUIRES(OnceCallableFunc<F>)
296 void schedulePlaced(F&& f, ForceQueuingTag);
297
298 // Shared body for scheduleBulk/scheduleBulkPlaced. kPlaced selects
299 // placed path (scheduleImplPlaced per-task) vs central queue (scheduleBulkEnqueue).
300 template <bool kPlaced, typename Generator>
301 void scheduleBulkImpl(size_t count, Generator&& gen);
302
303 // Bulk placed scheduling: chunked submit through the steal-ring path. Internal only —
304 // exposed via ConcurrentTaskSet's scheduleBulk under TaskCost::kHeavy routing.
305 template <typename Generator>
306 void scheduleBulkPlaced(size_t count, Generator&& gen);
307
308 // Core bulk enqueue: unconditionally stage, enqueue, and wake for a chunk of tasks.
309 // Caller is responsible for load factor checks. Count should be small (e.g. <= 2*numThreads).
310 // When a producer token is provided, uses token-based enqueue for better throughput.
311 template <typename Generator>
312 void
313 scheduleBulkEnqueue(size_t count, Generator&& gen, moodycamel::ProducerToken* token = nullptr);
314
315 // Wake enough threads to handle pending work. Uses PoolWakeState's budget-
316 // limited cascade for efficient parallel waking.
317 // Missed wakes are benign: the EpochWaiter's sleep timeout provides a safety
318 // net, so a missed wake only delays wakeup by up to that duration.
319 void conditionallyWake() {
320 auto* ws = detail::consumeLoad(wakeState_);
321 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
322 int32_t sleeping = ws->totalSleeping();
323 if (sleeping > 0) {
324 ssize_t pending = workRemaining_.load(std::memory_order_relaxed);
325 ssize_t numT = numThreads_.load(std::memory_order_relaxed);
326 ssize_t awake = numT - static_cast<ssize_t>(sleeping);
327 if (pending > awake) {
328 ws->claimAndWakeOne();
329 }
330 }
331 }
332 }
333
334 public:
335 // If we are not yet C++17, we provide aligned new/delete to avoid false sharing.
336#if __cplusplus < 201703L
337 static void* operator new(size_t sz) {
338 return detail::alignedMalloc(sz);
339 }
340 static void operator delete(void* ptr) {
341 return detail::alignedFree(ptr);
342 }
343#endif // __cplusplus
344
345 private:
346 // Per-thread ring buffer type for fork-join scheduling.
347 // 16 slots matches kAuto's oversubscription factor and fits in one cache line group.
348 using Ring = MpmcRingBuffer<OnceFunction, 16>;
349
350 // Steal ring configuration.
351 // Slots per thread: base capacity before sharing multiplier.
352 static constexpr size_t kStealSlotsPerThread = 4;
353 // Sharing factor: threads per steal ring (aligned with wake group size).
354#if defined(DISPENSO_TUNE_STEAL_RING_SHARING)
355 static constexpr size_t kStealRingSharing = DISPENSO_TUNE_STEAL_RING_SHARING;
356#else
357 // Matches the wake group-size default (8). See docs/development/architecture/wake_tuning.md.
358 static constexpr size_t kStealRingSharing = 8;
359#endif
360 static constexpr size_t kStealRingCapacity = kStealSlotsPerThread * kStealRingSharing;
361 using StealRing = MpmcRingBuffer<OnceFunction, kStealRingCapacity>;
362
363 // Cross-ring steal gate: workers only probe other rings' has-work bitmask
364 // after `failCount` consecutive empty pops on their own ring. Preserves
365 // placed-scheduling locality during steady-state operation; the threshold
366 // (~kSpinCheckInterval / 2) means we steal cross-ring only after sustained
367 // local idle, by which point our own ring's locality is exhausted anyway.
368#if defined(DISPENSO_TUNE_CROSS_RING_FAIL_THRESHOLD)
369 static constexpr int kCrossRingFailThreshold = DISPENSO_TUNE_CROSS_RING_FAIL_THRESHOLD;
370#else
371 static constexpr int kCrossRingFailThreshold = 32;
372#endif
373
374 // Enqueue a task to the central concurrent queue with optional producer token.
375 // Handles TSAN annotations. Used by scheduleBulkToRings for overflow tasks.
376 // moodycamel::ConcurrentQueue::enqueue returns false only on allocation
377 // failure; we propagate that as std::bad_alloc so callers don't silently
378 // drop work (which would deadlock TaskSet::wait via inflated outstanding
379 // counts). Out-of-memory recovery from individual small allocs is not
380 // tractable in general; throwing lets the application unwind or terminate.
381 DISPENSO_INLINE void enqueueToCentralQueue(OnceFunction task, moodycamel::ProducerToken* token) {
382 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
383 bool enqueued;
384 if (token) {
385 enqueued = work_.enqueue(*token, std::move(task));
386 } else {
387 enqueued = work_.enqueue(std::move(task));
388 }
389 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
390 if (DISPENSO_EXPECT(!enqueued, false)) {
391#if defined(__cpp_exceptions)
392 throw std::bad_alloc();
393#else
394 std::abort();
395#endif
396 }
397 // Mark queue as possibly-non-empty so spinning workers will try_dequeue.
398 centralQueueNonEmpty_.store(true, std::memory_order_relaxed);
399 }
400
401 // Push task i to ring i (linear layout) for fork-join scheduling.
402 // Tasks that don't fit in their ring go to central queue via fallbackToken.
403 // Handles workRemaining_ accounting and waking (one batch wake at the end).
404 // outstandingTaskCount_ must be managed by the caller.
405 template <typename Generator>
406 void scheduleBulkToRings(size_t count, Generator&& gen, moodycamel::ProducerToken* fallbackToken);
407
408 template <typename Generator>
409 DISPENSO_INLINE void scheduleBulkToRingsFastPath(
410 size_t count,
411 size_t ringCount,
412 Generator&& gen,
413 moodycamel::ProducerToken* fallbackToken);
414
415 template <typename Generator>
416 DISPENSO_INLINE void scheduleBulkToRingsBatched(
417 size_t count,
418 size_t ringCount,
419 size_t tasksPerRing,
420 Generator&& gen,
421 moodycamel::ProducerToken* fallbackToken);
422
423 // Shared work-finding logic for both loop variants. Checks own ring,
424 // central queue, and steal ring as third tier.
425 // Returns true if work was found and executed.
426 // preferRing: sticky hint — true = try ring first, false = try central queue first.
427 // failCount: consecutive failures finding work — used to gate cross-ring stealing
428 // so we preserve placed-scheduling locality during steady-state operation.
429 DISPENSO_INLINE bool tryFindAndExecuteWork(
430 Ring& myRing,
431 StealRing& myStealRing,
432 size_t myStealIdx,
433 moodycamel::ConsumerToken& ctoken,
434 bool& preferRing,
435 int failCount,
436 bool checkQueue = true);
437
438 // Number of tasks each thread accumulates before flushing workRemaining_.
439 // This batching reduces atomic contention in threadLoop, but inflates
440 // workRemaining_ by up to kWorkBatchSize * numThreads, so poolLoadFactor_
441 // must be reduced accordingly for accurate load-shedding in schedule().
442 static constexpr int kWorkBatchSize = 8;
443
444 // Minimum spinning threads before we skip waking a sleeper. If fewer than
445 // this many threads are spinning, we wake a sleeper to ensure coverage.
446 // Higher = more aggressive waking; lower = trust spinners more.
447 static constexpr int32_t kSpinnerWakeThreshold = 2;
448
449 mutable std::mutex threadsMutex_;
450 std::deque<PerThreadData> threads_;
451 size_t poolLoadMultiplier_;
452
453 // These atomics are read frequently in the hot schedule() path, so they need
454 // cache-line alignment to avoid false sharing with the mutex/deque above.
455 alignas(kCacheLineSize) std::atomic<ssize_t> poolLoadFactor_;
456 std::atomic<ssize_t> numThreads_;
457
458 moodycamel::ConcurrentQueue<OnceFunction> work_;
459
460 // Approximate flag indicating the central queue may have work. Set (store
461 // true) after enqueue, cleared (store false) when try_dequeue finds the queue
462 // empty. Used to gate try_dequeue in tryFindAndExecuteWork: a relaxed load
463 // replaces the expensive try_dequeue CAS when the queue is known empty,
464 // eliminating CAS contention from idle spinning threads. No atomic RMW —
465 // only plain stores and loads — so no contention on the flag itself.
466 // A clear can race an enqueue and drop it: the clearing worker decides the
467 // queue is empty, a producer enqueues and sets the flag, and the clear then
468 // overwrites that store. The task stays queued with the flag reading empty,
469 // and no spinning worker will look at the queue again. Recovery is the
470 // timeout-wake probe in threadLoopImpl, which bounds the delay to one sleep
471 // period; do not treat a false negative as self-correcting on its own.
472 alignas(kCacheLineSize) std::atomic<bool> centralQueueNonEmpty_{false};
473
474 alignas(kCacheLineSize) std::atomic<ssize_t> workRemaining_{0};
475
476 alignas(kCacheLineSize) std::atomic<bool> enableEpochWaiter_{kDefaultWakeupEnable};
477 std::atomic<uint32_t> sleepLengthUs_{kDefaultSleepLenUs};
478
479 // Per-thread wake infrastructure: EpochWaiters, sleep masks, budget cascade.
480 // Atomic pointer — schedule paths load with relaxed ordering.
481 //
482 // Retired PoolWakeState objects are kept alive for the lifetime of the pool
483 // (grow-only graveyard); they are intentionally NOT freed at resize. schedule()
484 // reads wakeState_ lock-free (no threadsMutex_) and dereferences it (e.g.
485 // ws->totalSleeping()), so freeing a retired generation while a concurrent
486 // schedule() may still hold that pointer is a use-after-free. resize() joins all
487 // *pool* threads before swapping wakeState_, but external (non-pool) schedule()
488 // callers race the swap, so a retired object must outlive any such in-flight
489 // reader. Without a safe-reclamation protocol, never freeing during operation is
490 // the only correct option (an earlier bounded variant that freed old
491 // generations was reverted after TSAN caught exactly this free-vs-read race).
492 //
493 // Cost: one PoolWakeState (~O(numThreads)) is retained per resize() until the
494 // pool is destroyed. resize() is expected to be rare, so this is bounded in
495 // practice; only a process performing hundreds of thousands of resizes would
496 // accumulate meaningful memory. See
497 // docs/development/roadmap/core_scheduling.md ("Bounded PoolWakeState
498 // reclamation") for the planned asymmetric-fence / hazard-pointer
499 // scheme to bound this without taxing the schedule() hot path.
500 std::atomic<detail::PoolWakeState*> wakeState_{nullptr};
501 std::vector<decltype(detail::makeAligned<detail::PoolWakeState>(0))> wakeStateGraveyard_;
502
503 // Per-thread rings for fork-join scheduling. ConcurrentObjectArena provides
504 // stable pointers (grow-only, never freed), eliminating the need for a resize
505 // lock on the schedule path. Threads check own ring first in the steal order.
506 ConcurrentObjectArena<Ring> rings_;
507 std::atomic<size_t> numRings_{0};
508
509 // Steal rings for non-locality work distribution.
510 // Populated by schedule() (both proactive wake and no-sleeper paths).
511 // Consumed in tryFindAndExecuteWork (third tier) and outer thread loop.
512 //
513 // stealRingSharing_: threads per steal ring (default kStealRingSharing).
514 // Ring capacity = kStealSlotsPerThread * kStealRingSharing.
515 ConcurrentObjectArena<StealRing> stealRings_;
516 std::atomic<size_t> numStealRings_{0};
517 size_t stealRingSharing_{kStealRingSharing};
518
519 // Sparse hint for which steal rings have work. Bit i set means
520 // stealRings_[i] may have work. Set on push (idempotent fetch_or); lazily
521 // cleared by consumers that find a ring empty after popping or that
522 // observe an empty ring at scan time. False positives are benign (just a
523 // wasted try_pop); false negatives are not possible because every
524 // successful push sets the bit before the work is observable.
525 // Pools with >64 steal rings (512+ threads at kStealRingSharing=8) degrade
526 // gracefully: rings beyond index 63 still receive work via try_push, but
527 // aren't tracked in this bitmask, so cross-ring stealing falls back to
528 // local-ring-only polling for those rings.
529 static constexpr size_t kMaxStealRings = 64;
530 alignas(kCacheLineSize) std::atomic<uint64_t> stealRingsWithWork_{0};
531
532 // Threads not currently in their inner work loop (spinning or sleeping).
533 // Incremented when a thread exhausts work (exits inner loop with nothing found).
534 // Decremented when a thread finds work (enters inner loop) or at thread exit.
535 // Updated at burst boundaries (not per-task), so contention is low.
536 // Used by schedule paths to skip wake calls when spinners exist.
537 alignas(kCacheLineSize) std::atomic<int32_t> numNotWorking_{0};
538
539#if defined DISPENSO_DEBUG
540 alignas(kCacheLineSize) std::atomic<ssize_t> outstandingTaskSets_{0};
541#endif // DISPENSO_DEBUG
542
543 friend class ConcurrentTaskSet;
544 friend class TaskSet;
545 friend class TaskSetBase;
546
547 template <typename Result>
548 friend class detail::FutureBase;
549 template <typename Result>
550 friend class detail::FutureImplBase;
551};
552
558DISPENSO_DLL_ACCESS ThreadPool& globalThreadPool();
559
565DISPENSO_DLL_ACCESS void resizeGlobalThreadPool(size_t numThreads);
566
567// ----------------------------- Implementation details -------------------------------------
568
569DISPENSO_INLINE bool ThreadPool::shouldRunInline() {
570 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
571 ssize_t quickLoadFactor = numThreads_.load(std::memory_order_relaxed);
572 quickLoadFactor += quickLoadFactor / 2;
573 return (detail::PerPoolPerThreadInfo::isPoolRecursive(this) && curWork > quickLoadFactor) ||
574 (curWork > poolLoadFactor_.load(std::memory_order_relaxed));
575}
576
577template <bool kPlaced, typename F>
578inline void ThreadPool::forceEnqueue(F&& f, moodycamel::ProducerToken* token) {
579 if (!numThreads_.load(std::memory_order_relaxed)) {
580 f();
581 return;
582 }
583 workRemaining_.fetch_add(1, std::memory_order_release);
584 if (kPlaced) {
585 scheduleImplPlaced({std::forward<F>(f)}, token);
586 } else {
587 scheduleImpl({std::forward<F>(f)}, token);
588 }
589}
590
591template <typename F>
592DISPENSO_REQUIRES(OnceCallableFunc<F>)
593inline void ThreadPool::schedule(F&& f) {
594 if (shouldRunInline()) {
595 f();
596 } else {
597 schedule(std::forward<F>(f), ForceQueuingTag());
598 }
599}
600
601template <typename F>
602DISPENSO_REQUIRES(OnceCallableFunc<F>)
603inline void ThreadPool::schedule(F&& f, ForceQueuingTag) {
604 auto* token =
605 static_cast<moodycamel::ProducerToken*>(detail::PerPoolPerThreadInfo::producer(this));
606 forceEnqueue<false>(std::forward<F>(f), token);
607}
608
609template <typename F>
610inline void ThreadPool::schedule(moodycamel::ProducerToken& token, F&& f) {
611 if (shouldRunInline()) {
612 f();
613 } else {
614 schedule(token, std::forward<F>(f), ForceQueuingTag());
615 }
616}
617
618template <typename F>
619inline void ThreadPool::schedule(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
620 forceEnqueue<false>(std::forward<F>(f), &token);
621}
622
623template <typename F>
624DISPENSO_REQUIRES(OnceCallableFunc<F>)
625inline void ThreadPool::schedulePlaced(F&& f) {
626 if (shouldRunInline()) {
627 f();
628 } else {
629 schedulePlaced(std::forward<F>(f), ForceQueuingTag());
630 }
631}
632
633template <typename F>
634DISPENSO_REQUIRES(OnceCallableFunc<F>)
635inline void ThreadPool::schedulePlaced(F&& f, ForceQueuingTag) {
636 auto* token =
637 static_cast<moodycamel::ProducerToken*>(detail::PerPoolPerThreadInfo::producer(this));
638 forceEnqueue<true>(std::forward<F>(f), token);
639}
640
641template <typename F>
642inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f) {
643 if (shouldRunInline()) {
644 f();
645 } else {
646 schedulePlaced(token, std::forward<F>(f), ForceQueuingTag());
647 }
648}
649
650template <typename F>
651inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
652 forceEnqueue<true>(std::forward<F>(f), &token);
653}
654
655DISPENSO_INLINE void ThreadPool::scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token) {
656 enqueueToCentralQueue(std::move(task), token);
657
658 // Wake when pending work exceeds awake threads. Each schedule()
659 // call wakes at most one sleeper — matching baseline's wake-per-task
660 // cadence but using per-thread futexes. The one-at-a-time approach
661 // avoids thundering herd on the central queue while still waking
662 // threads proportionally to submitted work over a burst.
663 auto* ws = detail::consumeLoad(wakeState_);
664 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
665 int32_t sleeping = ws->totalSleeping();
666 if (sleeping > 0) {
667 ssize_t pending = workRemaining_.load(std::memory_order_relaxed);
668 ssize_t numT = numThreads_.load(std::memory_order_relaxed);
669 ssize_t awake = numT - static_cast<ssize_t>(sleeping);
670 if (pending > awake) {
671 ws->claimAndWakeOne();
672 }
673 }
674 }
675}
676
677DISPENSO_INLINE void ThreadPool::scheduleImplPlaced(
678 OnceFunction task,
679 moodycamel::ProducerToken* token) {
680 // Proactive wake: claim a sleeping thread and push to its steal ring.
681 auto* ws = detail::consumeLoad(wakeState_);
682 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
683 int32_t sleeping = ws->totalSleeping();
684 if (sleeping > 0 &&
685 numNotWorking_.load(std::memory_order_relaxed) - sleeping < kSpinnerWakeThreshold) {
686 int32_t wokeThread = ws->claimAndWakeOne();
687 if (wokeThread >= 0) {
688 size_t stealIdx = static_cast<size_t>(wokeThread) / stealRingSharing_;
689 if (stealIdx < numStealRings_.load(std::memory_order_relaxed) &&
690 stealRings_[stealIdx].try_push(std::move(task))) {
691 if (stealIdx < kMaxStealRings) {
692 stealRingsWithWork_.fetch_or(uint64_t{1} << stealIdx, std::memory_order_release);
693 }
694 return;
695 }
696 }
697 }
698 }
699
700 // Central queue fallback.
701 enqueueToCentralQueue(std::move(task), token);
702
703 conditionallyWake();
704}
705
706inline bool ThreadPool::tryExecuteNext() {
707 OnceFunction next;
708 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
709 bool dequeued = work_.try_dequeue(next);
710 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
711 if (dequeued) {
712 executeNext(std::move(next));
713 return true;
714 }
715 return false;
716}
717
718inline bool ThreadPool::tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token) {
719 OnceFunction next;
720 if (work_.try_dequeue_from_producer(token, next)) {
721 executeNext(std::move(next));
722 return true;
723 }
724 return false;
725}
726
727inline bool ThreadPool::tryExecuteNextFromRings(size_t& startRing) {
728 OnceFunction task;
729 // Acquire pairs with resizeLocked's numRings_.store(release), which is published
730 // only after the new rings are fully constructed in the arena. A relaxed load
731 // could observe the grown count without the rings' construction being visible,
732 // letting us index a not-yet-constructed ring (UB; SIGILL on weak-memory targets
733 // like arm64).
734 size_t n = numRings_.load(std::memory_order_acquire);
735 for (size_t i = 0; i < n; ++i) {
736 size_t idx = (startRing + i) % n;
737 if (rings_[idx].try_pop(task)) {
738 startRing = idx;
739 executeNext(std::move(task));
740 return true;
741 }
742 }
743 startRing = 0;
744 return false;
745}
746
747inline void ThreadPool::executeNext(OnceFunction next) {
748 next();
749 workRemaining_.fetch_add(-1, std::memory_order_relaxed);
750}
751
752DISPENSO_INLINE bool ThreadPool::tryFindAndExecuteWork(
753 Ring& myRing,
754 StealRing& myStealRing,
755 size_t myStealIdx,
756 moodycamel::ConsumerToken& ctoken,
757 bool& preferRing,
758 int failCount,
759 bool checkQueue) {
760 OnceFunction task;
761 if (preferRing) {
762 bool fromRing = myRing.try_pop(task);
763 if (fromRing) {
764 task();
765 return true;
766 }
767 if (checkQueue && centralQueueNonEmpty_.load(std::memory_order_relaxed)) {
768 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
769 bool got = work_.try_dequeue(ctoken, task);
770 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
771 if (got) {
772 preferRing = false;
773 task();
774 return true;
775 }
776 // Empty on observation; clear flag (relaxed, plain store).
777 centralQueueNonEmpty_.store(false, std::memory_order_relaxed);
778 }
779 if (!myStealRing.empty() && myStealRing.try_pop(task)) {
780 task();
781 return true;
782 }
783 if (failCount >= kCrossRingFailThreshold) {
784 uint64_t mask = stealRingsWithWork_.load(std::memory_order_acquire);
785 if (mask != 0) {
786 if (myStealIdx < kMaxStealRings) {
787 mask &= ~(uint64_t{1} << myStealIdx);
788 }
789 if (mask != 0) {
790 int target = detail::countTrailingZeros(mask);
791 if (stealRings_[static_cast<size_t>(target)].try_pop(task)) {
792 task();
793 return true;
794 }
795 stealRingsWithWork_.fetch_and(~(uint64_t{1} << target), std::memory_order_relaxed);
796 }
797 }
798 }
799 } else {
800 if (checkQueue && centralQueueNonEmpty_.load(std::memory_order_relaxed)) {
801 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
802 bool got = work_.try_dequeue(ctoken, task);
803 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
804 if (got) {
805 task();
806 return true;
807 }
808 centralQueueNonEmpty_.store(false, std::memory_order_relaxed);
809 }
810 bool fromRing = myRing.try_pop(task);
811 if (fromRing) {
812 preferRing = true;
813 task();
814 return true;
815 }
816 }
817
818 return false;
819}
820
821template <typename Generator>
822DISPENSO_INLINE void ThreadPool::scheduleBulkToRingsFastPath(
823 size_t count,
824 size_t ringCount,
825 Generator&& gen,
826 moodycamel::ProducerToken* fallbackToken) {
827#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
828 // Pattern C cascade: wrap each cascade-host thread's task in a lambda
829 // that wakes its target group BEFORE running user work. Producer issues
830 // one wake-all on the seed group; the woken threads cascade in parallel
831 // to all other groups, then run their own user work.
832 auto* wsCascade = detail::consumeLoad(wakeState_);
833 bool useCascade = enableEpochWaiter_.load(std::memory_order_acquire) && wsCascade &&
834 wsCascade->totalSleeping() > 0;
835 for (size_t ring = 0; ring < count && ring < ringCount; ++ring) {
836 OnceFunction task = gen(ring);
837 int32_t target = useCascade
838 ? wsCascade->cascadeTargetFor(static_cast<int32_t>(ring), static_cast<int32_t>(count))
839 : -1;
840 if (target >= 0) {
841 OnceFunction wrapped = [wsCascade, target, inner = std::move(task)]() mutable {
842 wsCascade->cascadeWake(target);
843 inner();
844 };
845 if (!rings_[ring].try_push(std::move(wrapped))) {
846 enqueueToCentralQueue(std::move(wrapped), fallbackToken);
847 }
848 } else {
849 if (!rings_[ring].try_push(std::move(task))) {
850 enqueueToCentralQueue(std::move(task), fallbackToken);
851 }
852 }
853 }
854#else
855 for (size_t ring = 0; ring < count && ring < ringCount; ++ring) {
856 OnceFunction task = gen(ring);
857 if (!rings_[ring].try_push(std::move(task))) {
858 enqueueToCentralQueue(std::move(task), fallbackToken);
859 }
860 }
861#endif
862}
863
864template <typename Generator>
865DISPENSO_INLINE void ThreadPool::scheduleBulkToRingsBatched(
866 size_t count,
867 size_t ringCount,
868 size_t tasksPerRing,
869 Generator&& gen,
870 moodycamel::ProducerToken* fallbackToken) {
871 constexpr size_t kMaxStage = Ring::capacity();
872 size_t taskIdx = 0;
873 for (size_t ring = 0; ring < ringCount && taskIdx < count; ++ring) {
874 size_t blockEnd = std::min(taskIdx + tasksPerRing, count);
875 size_t blockSize = blockEnd - taskIdx;
876
877 size_t toStage = std::min(blockSize, kMaxStage);
878 OnceFunction staged[kMaxStage];
879 for (size_t j = 0; j < toStage; ++j) {
880 staged[j] = gen(taskIdx + j);
881 }
882
883 size_t pushed = rings_[ring].try_push_batch(staged, toStage);
884
885 for (size_t j = pushed; j < toStage; ++j) {
886 enqueueToCentralQueue(std::move(staged[j]), fallbackToken);
887 }
888
889 for (size_t j = taskIdx + toStage; j < blockEnd; ++j) {
890 enqueueToCentralQueue(gen(j), fallbackToken);
891 }
892 taskIdx += blockSize;
893 }
894}
895
896template <typename Generator>
897void ThreadPool::scheduleBulkToRings(
898 size_t count,
899 Generator&& gen,
900 moodycamel::ProducerToken* fallbackToken) {
901 if (count == 0) {
902 return;
903 }
904 assert(count <= numRings_.load(std::memory_order_relaxed));
905
906 workRemaining_.fetch_add(static_cast<ssize_t>(count), std::memory_order_release);
907
908 // Acquire: see tryExecuteNextFromRings. Pairs with the release store in
909 // resizeLocked so we observe the freshly-constructed rings, not merely the
910 // updated count.
911 size_t ringCount = numRings_.load(std::memory_order_acquire);
912 size_t tasksPerRing = (count + ringCount - 1) / ringCount;
913
914 if (tasksPerRing <= 1) {
915 scheduleBulkToRingsFastPath(count, ringCount, std::forward<Generator>(gen), fallbackToken);
916 } else {
917 scheduleBulkToRingsBatched(
918 count, ringCount, tasksPerRing, std::forward<Generator>(gen), fallbackToken);
919 }
920
921 auto* ws = detail::consumeLoad(wakeState_);
922 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
923#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
924 ws->cascadeWakeSeed(static_cast<int32_t>(count));
925#else
926 ws->wakeRange(static_cast<int32_t>(count));
927#endif
928 }
929}
930
931namespace detail {
932// Generating iterator for scheduleBulkEnqueue. Produces OnceFunction objects
933// on-the-fly during enqueue_bulk, avoiding the need for a staging buffer.
934// moodycamel's enqueue_bulk uses single-pass input iterator semantics.
935template <typename Generator>
936struct BulkGenIter {
937 using difference_type = std::ptrdiff_t;
938 using value_type = OnceFunction;
939 using pointer = OnceFunction*;
940 using reference = OnceFunction&;
941 using iterator_category = std::input_iterator_tag;
942
943 Generator* gen;
944 size_t index;
945 OnceFunction operator*() {
946 return (*gen)(index);
947 }
948 BulkGenIter& operator++() {
949 ++index;
950 return *this;
951 }
952 BulkGenIter operator++(int) {
953 BulkGenIter tmp = *this;
954 ++index;
955 return tmp;
956 }
957};
958} // namespace detail
959
960template <typename Generator>
961void ThreadPool::scheduleBulkEnqueue(
962 size_t count,
963 Generator&& gen,
964 moodycamel::ProducerToken* token) {
965 detail::BulkGenIter<typename std::remove_reference<Generator>::type> it{&gen, 0};
966
967 // Single atomic update + bulk enqueue
968 workRemaining_.fetch_add(static_cast<ssize_t>(count), std::memory_order_release);
969
970 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
971 bool enqueued;
972 if (token) {
973 enqueued = work_.enqueue_bulk(*token, it, count);
974 } else {
975 enqueued = work_.enqueue_bulk(it, count);
976 }
977 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
978 if (DISPENSO_EXPECT(!enqueued, false)) {
979 workRemaining_.fetch_sub(static_cast<ssize_t>(count), std::memory_order_relaxed);
980#if defined(__cpp_exceptions)
981 throw std::bad_alloc();
982#else
983 std::abort();
984#endif
985 }
986 // Mark queue as possibly-non-empty so spinning workers will try_dequeue.
987 centralQueueNonEmpty_.store(true, std::memory_order_relaxed);
988
989 // Wake appropriate threads. Cap by actual sleeping count to avoid over-waking.
990 // Spinning threads (numNotWorking - totalSleeping) will find enqueued work
991 // naturally, so only wake enough sleepers to cover the deficit beyond the
992 // spinner threshold.
993 auto* ws = detail::consumeLoad(wakeState_);
994 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
995 int32_t sleeping = ws->totalSleeping();
996 if (sleeping > 0) {
997 int32_t notWorking = numNotWorking_.load(std::memory_order_relaxed);
998 int32_t spinning = std::max(int32_t{0}, notWorking - sleeping);
999 // Only count spinners beyond the threshold as "covering" tasks
1000 int32_t effectiveSpinners = std::max(int32_t{0}, spinning - kSpinnerWakeThreshold + 1);
1001 int32_t toWake = std::max(int32_t{0}, static_cast<int32_t>(count) - effectiveSpinners);
1002 toWake = std::min(toWake, sleeping);
1003 if (toWake <= ws->branchFactor()) {
1004 // Small N: direct claim+wake, no cascade overhead.
1005 for (int32_t i = 0; i < toWake; ++i) {
1006 if (ws->claimAndWakeOne() < 0) {
1007 break;
1008 }
1009 }
1010 } else {
1011 // Large N: Pattern C cascade. Producer wakes seed g0; cascade-host
1012 // threads wake their target groups in parallel as they spin up.
1013 // Central-queue work is found by the standard tryFindAndExecuteWork
1014 // loop on each woken thread — no per-thread ring pre-staging needed.
1015 ws->cascadeWakeSeed(toWake);
1016 }
1017 }
1018 }
1019}
1020
1021template <bool kPlaced, typename Generator>
1022void ThreadPool::scheduleBulkImpl(size_t count, Generator&& gen) {
1023 if (count == 0) {
1024 return;
1025 }
1026
1027 ssize_t numPool = numThreads_.load(std::memory_order_relaxed);
1028 if (!numPool) {
1029 for (size_t i = 0; i < count; ++i) {
1030 gen(i)();
1031 }
1032 return;
1033 }
1034
1035 // Process in chunks, interleaving enqueue and inline execution based on load.
1036 size_t chunkSize = static_cast<size_t>(numPool) + static_cast<size_t>(numPool) / 2;
1037 size_t i = 0;
1038 while (i < count) {
1039 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
1040 ssize_t loadFactor = poolLoadFactor_.load(std::memory_order_relaxed);
1041 if (curWork > loadFactor) {
1042 gen(i)();
1043 ++i;
1044 } else {
1045 ssize_t room = loadFactor - curWork;
1046 size_t toEnqueue = std::min({count - i, chunkSize, static_cast<size_t>(room)});
1047 if (toEnqueue == 0) {
1048 toEnqueue = 1;
1049 }
1050 size_t base = i;
1051 if (kPlaced) {
1052 workRemaining_.fetch_add(static_cast<ssize_t>(toEnqueue), std::memory_order_release);
1053 for (size_t j = 0; j < toEnqueue; ++j) {
1054 scheduleImplPlaced({gen(base + j)}, nullptr);
1055 }
1056 } else {
1057 scheduleBulkEnqueue(toEnqueue, [&gen, base](size_t j) { return gen(base + j); });
1058 }
1059 i += toEnqueue;
1060 }
1061 }
1062}
1063
1064template <typename Generator>
1065void ThreadPool::scheduleBulk(size_t count, Generator&& gen) {
1066 scheduleBulkImpl<false>(count, std::forward<Generator>(gen));
1067}
1068
1069template <typename Generator>
1070void ThreadPool::scheduleBulkPlaced(size_t count, Generator&& gen) {
1071 scheduleBulkImpl<true>(count, std::forward<Generator>(gen));
1072}
1073
1074} // namespace dispenso
static constexpr size_type capacity() noexcept
Returns the maximum number of elements the buffer can hold.
void setSignalingWake(bool enable, const std::chrono::duration< Rep, Period > &sleepDuration=std::chrono::microseconds(kDefaultSleepLenUs))
DISPENSO_DLL_ACCESS ~ThreadPool()
ssize_t numThreads() const
DISPENSO_DLL_ACCESS void resize(ssize_t n) DISPENSO_NO_THREAD_SAFETY_ANALYSIS
void scheduleBulk(size_t count, Generator &&gen)
DISPENSO_DLL_ACCESS ThreadPool(size_t n, size_t poolLoadMultiplier=32)
constexpr size_t kCacheLineSize
A constant that defines a safe number of bytes+alignment to avoid false sharing.
Definition platform.h:125
DISPENSO_DLL_ACCESS ThreadPool & globalThreadPool()
DISPENSO_DLL_ACCESS void resizeGlobalThreadPool(size_t numThreads)