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(__APPLE__) || 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, via PoolWakeState.
316 // Missed wakes are benign: the EpochWaiter's sleep timeout provides a safety
317 // net, so a missed wake only delays wakeup by up to that duration.
318 void conditionallyWake() {
319 auto* ws = detail::consumeLoad(wakeState_);
320 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
321 int32_t sleeping = ws->totalSleeping();
322 if (sleeping > 0) {
323 ssize_t pending = workRemaining_.load(std::memory_order_relaxed);
324 ssize_t numT = numThreads_.load(std::memory_order_relaxed);
325 ssize_t awake = numT - static_cast<ssize_t>(sleeping);
326 if (pending > awake) {
327 ws->claimAndWakeOne();
328 }
329 }
330 }
331 }
332
333 public:
334 // If we are not yet C++17, we provide aligned new/delete to avoid false sharing.
335#if DISPENSO_CPLUSPLUS < 201703L
336 static void* operator new(size_t sz) {
337 return detail::alignedMallocOrThrow(sz);
338 }
339 static void operator delete(void* ptr) {
340 return detail::alignedFree(ptr);
341 }
342#endif // DISPENSO_CPLUSPLUS
343
344 private:
345 // Per-thread ring buffer type for fork-join scheduling.
346 // 16 slots matches ChunkedRange::calcChunkSize's default maxDynFactor (at most 16 chunks per
347 // working thread, parallel_for.h); tasks that do not fit fall back to the central queue.
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 // PerThreadData is cache-line aligned; before C++17 std::allocator would misalign it.
451 std::deque<PerThreadData, detail::AlignedAllocator<PerThreadData>> threads_;
452 size_t poolLoadMultiplier_;
453
454 // These atomics are read frequently in the hot schedule() path, so they need
455 // cache-line alignment to avoid false sharing with the mutex/deque above.
456 alignas(kCacheLineSize) std::atomic<ssize_t> poolLoadFactor_;
457 std::atomic<ssize_t> numThreads_;
458
459 moodycamel::ConcurrentQueue<OnceFunction> work_;
460
461 // Approximate flag indicating the central queue may have work. Set (store
462 // true) after enqueue, cleared (store false) when try_dequeue finds the queue
463 // empty. Used to gate try_dequeue in tryFindAndExecuteWork: a relaxed load
464 // replaces the expensive try_dequeue CAS when the queue is known empty,
465 // eliminating CAS contention from idle spinning threads. No atomic RMW —
466 // only plain stores and loads — so no contention on the flag itself.
467 // A clear can race an enqueue and drop it: the clearing worker decides the
468 // queue is empty, a producer enqueues and sets the flag, and the clear then
469 // overwrites that store. The task stays queued with the flag reading empty,
470 // and no spinning worker will look at the queue again. Recovery is the
471 // timeout-wake probe in threadLoopImpl, which bounds the delay to one sleep
472 // period; do not treat a false negative as self-correcting on its own.
473 alignas(kCacheLineSize) std::atomic<bool> centralQueueNonEmpty_{false};
474
475 alignas(kCacheLineSize) std::atomic<ssize_t> workRemaining_{0};
476
477 alignas(kCacheLineSize) std::atomic<bool> enableEpochWaiter_{kDefaultWakeupEnable};
478 std::atomic<uint32_t> sleepLengthUs_{kDefaultSleepLenUs};
479
480 // Wake infrastructure: per-group EpochWaiters and sleep masks, cascade table.
481 // Atomic pointer — schedule paths load with relaxed ordering.
482 //
483 // Retired PoolWakeState objects are kept alive for the lifetime of the pool
484 // (grow-only graveyard); they are intentionally NOT freed at resize. schedule()
485 // reads wakeState_ lock-free (no threadsMutex_) and dereferences it (e.g.
486 // ws->totalSleeping()), so freeing a retired generation while a concurrent
487 // schedule() may still hold that pointer is a use-after-free. resize() joins all
488 // *pool* threads before swapping wakeState_, but external (non-pool) schedule()
489 // callers race the swap, so a retired object must outlive any such in-flight
490 // reader. Without a safe-reclamation protocol, never freeing during operation is
491 // the only correct option (an earlier bounded variant that freed old
492 // generations was reverted after TSAN caught exactly this free-vs-read race).
493 //
494 // Cost: one PoolWakeState (~O(numThreads)) is retained per resize() until the
495 // pool is destroyed. resize() is expected to be rare, so this is bounded in
496 // practice; only a process performing hundreds of thousands of resizes would
497 // accumulate meaningful memory. See
498 // docs/development/roadmap/core_scheduling.md ("Bounded PoolWakeState
499 // reclamation") for the planned asymmetric-fence / hazard-pointer
500 // scheme to bound this without taxing the schedule() hot path.
501 std::atomic<detail::PoolWakeState*> wakeState_{nullptr};
502 std::vector<decltype(detail::makeAligned<detail::PoolWakeState>(0))> wakeStateGraveyard_;
503
504 // Per-thread rings for fork-join scheduling. ConcurrentObjectArena provides
505 // stable pointers (grow-only, never freed), eliminating the need for a resize
506 // lock on the schedule path. Threads check own ring first in the steal order.
507 ConcurrentObjectArena<Ring> rings_;
508 std::atomic<size_t> numRings_{0};
509
510 // Steal rings for non-locality work distribution.
511 // Populated by schedule() (both proactive wake and no-sleeper paths).
512 // Consumed in tryFindAndExecuteWork (third tier) and outer thread loop.
513 //
514 // stealRingSharing_: threads per steal ring (default kStealRingSharing).
515 // Ring capacity = kStealSlotsPerThread * kStealRingSharing.
516 ConcurrentObjectArena<StealRing> stealRings_;
517 std::atomic<size_t> numStealRings_{0};
518 size_t stealRingSharing_{kStealRingSharing};
519
520 // Sparse hint for which steal rings have work. Bit i set means
521 // stealRings_[i] may have work. Set on push (idempotent fetch_or); lazily
522 // cleared by consumers that find a ring empty after popping or that
523 // observe an empty ring at scan time. False positives are benign (just a
524 // wasted try_pop); false negatives are not possible because every
525 // successful push sets the bit before the work is observable.
526 // Pools with >64 steal rings (512+ threads at kStealRingSharing=8) degrade
527 // gracefully: rings beyond index 63 still receive work via try_push, but
528 // aren't tracked in this bitmask, so cross-ring stealing falls back to
529 // local-ring-only polling for those rings.
530 static constexpr size_t kMaxStealRings = 64;
531 alignas(kCacheLineSize) std::atomic<uint64_t> stealRingsWithWork_{0};
532
533 // Threads not currently in their inner work loop (spinning or sleeping).
534 // Incremented when a thread exhausts work (exits inner loop with nothing found).
535 // Decremented when a thread finds work (enters inner loop) or at thread exit.
536 // Updated at burst boundaries (not per-task), so contention is low.
537 // Used by schedule paths to skip wake calls when spinners exist.
538 alignas(kCacheLineSize) std::atomic<int32_t> numNotWorking_{0};
539
540#if defined DISPENSO_DEBUG
541 alignas(kCacheLineSize) std::atomic<ssize_t> outstandingTaskSets_{0};
542#endif // DISPENSO_DEBUG
543
544 friend class ConcurrentTaskSet;
545 friend class TaskSet;
546 friend class TaskSetBase;
547
548 template <typename Result>
549 friend class detail::FutureBase;
550 template <typename Result>
551 friend class detail::FutureImplBase;
552};
553
559DISPENSO_DLL_ACCESS ThreadPool& globalThreadPool();
560
566DISPENSO_DLL_ACCESS void resizeGlobalThreadPool(size_t numThreads);
567
568// ----------------------------- Implementation details -------------------------------------
569
570DISPENSO_INLINE bool ThreadPool::shouldRunInline() {
571 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
572 ssize_t quickLoadFactor = numThreads_.load(std::memory_order_relaxed);
573 quickLoadFactor += quickLoadFactor / 2;
574 return (detail::PerPoolPerThreadInfo::isPoolRecursive(this) && curWork > quickLoadFactor) ||
575 (curWork > poolLoadFactor_.load(std::memory_order_relaxed));
576}
577
578template <bool kPlaced, typename F>
579inline void ThreadPool::forceEnqueue(F&& f, moodycamel::ProducerToken* token) {
580 if (!numThreads_.load(std::memory_order_relaxed)) {
581 f();
582 return;
583 }
584 workRemaining_.fetch_add(1, std::memory_order_release);
585 if (kPlaced) {
586 scheduleImplPlaced({std::forward<F>(f)}, token);
587 } else {
588 scheduleImpl({std::forward<F>(f)}, token);
589 }
590}
591
592template <typename F>
593DISPENSO_REQUIRES(OnceCallableFunc<F>)
594inline void ThreadPool::schedule(F&& f) {
595 if (shouldRunInline()) {
596 f();
597 } else {
598 schedule(std::forward<F>(f), ForceQueuingTag());
599 }
600}
601
602template <typename F>
603DISPENSO_REQUIRES(OnceCallableFunc<F>)
604inline void ThreadPool::schedule(F&& f, ForceQueuingTag) {
605 auto* token =
606 static_cast<moodycamel::ProducerToken*>(detail::PerPoolPerThreadInfo::producer(this));
607 forceEnqueue<false>(std::forward<F>(f), token);
608}
609
610template <typename F>
611inline void ThreadPool::schedule(moodycamel::ProducerToken& token, F&& f) {
612 if (shouldRunInline()) {
613 f();
614 } else {
615 schedule(token, std::forward<F>(f), ForceQueuingTag());
616 }
617}
618
619template <typename F>
620inline void ThreadPool::schedule(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
621 forceEnqueue<false>(std::forward<F>(f), &token);
622}
623
624template <typename F>
625DISPENSO_REQUIRES(OnceCallableFunc<F>)
626inline void ThreadPool::schedulePlaced(F&& f) {
627 if (shouldRunInline()) {
628 f();
629 } else {
630 schedulePlaced(std::forward<F>(f), ForceQueuingTag());
631 }
632}
633
634template <typename F>
635DISPENSO_REQUIRES(OnceCallableFunc<F>)
636inline void ThreadPool::schedulePlaced(F&& f, ForceQueuingTag) {
637 auto* token =
638 static_cast<moodycamel::ProducerToken*>(detail::PerPoolPerThreadInfo::producer(this));
639 forceEnqueue<true>(std::forward<F>(f), token);
640}
641
642template <typename F>
643inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f) {
644 if (shouldRunInline()) {
645 f();
646 } else {
647 schedulePlaced(token, std::forward<F>(f), ForceQueuingTag());
648 }
649}
650
651template <typename F>
652inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
653 forceEnqueue<true>(std::forward<F>(f), &token);
654}
655
656DISPENSO_INLINE void ThreadPool::scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token) {
657 enqueueToCentralQueue(std::move(task), token);
658
659 // Wake when pending work exceeds awake threads. Each schedule()
660 // call wakes at most one sleeper — matching baseline's wake-per-task
661 // cadence but using per-thread futexes. The one-at-a-time approach
662 // avoids thundering herd on the central queue while still waking
663 // threads proportionally to submitted work over a burst.
664 auto* ws = detail::consumeLoad(wakeState_);
665 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
666 int32_t sleeping = ws->totalSleeping();
667 if (sleeping > 0) {
668 ssize_t pending = workRemaining_.load(std::memory_order_relaxed);
669 ssize_t numT = numThreads_.load(std::memory_order_relaxed);
670 ssize_t awake = numT - static_cast<ssize_t>(sleeping);
671 if (pending > awake) {
672 ws->claimAndWakeOne();
673 }
674 }
675 }
676}
677
678DISPENSO_INLINE void ThreadPool::scheduleImplPlaced(
679 OnceFunction task,
680 moodycamel::ProducerToken* token) {
681 // Proactive wake: claim a sleeping thread and push to its steal ring.
682 auto* ws = detail::consumeLoad(wakeState_);
683 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
684 int32_t sleeping = ws->totalSleeping();
685 if (sleeping > 0 &&
686 numNotWorking_.load(std::memory_order_relaxed) - sleeping < kSpinnerWakeThreshold) {
687 int32_t wokeThread = ws->claimAndWakeOne();
688 if (wokeThread >= 0) {
689 size_t stealIdx = static_cast<size_t>(wokeThread) / stealRingSharing_;
690 if (stealIdx < numStealRings_.load(std::memory_order_relaxed) &&
691 stealRings_[stealIdx].try_push(std::move(task))) {
692 if (stealIdx < kMaxStealRings) {
693 stealRingsWithWork_.fetch_or(uint64_t{1} << stealIdx, std::memory_order_release);
694 }
695 return;
696 }
697 }
698 }
699 }
700
701 // Central queue fallback.
702 enqueueToCentralQueue(std::move(task), token);
703
704 conditionallyWake();
705}
706
707inline bool ThreadPool::tryExecuteNext() {
708 OnceFunction next;
709 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
710 bool dequeued = work_.try_dequeue(next);
711 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
712 if (dequeued) {
713 executeNext(std::move(next));
714 return true;
715 }
716 return false;
717}
718
719inline bool ThreadPool::tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token) {
720 OnceFunction next;
721 if (work_.try_dequeue_from_producer(token, next)) {
722 executeNext(std::move(next));
723 return true;
724 }
725 return false;
726}
727
728inline bool ThreadPool::tryExecuteNextFromRings(size_t& startRing) {
729 OnceFunction task;
730 // Acquire pairs with resizeLocked's numRings_.store(release), which is published
731 // only after the new rings are fully constructed in the arena. A relaxed load
732 // could observe the grown count without the rings' construction being visible,
733 // letting us index a not-yet-constructed ring (UB; SIGILL on weak-memory targets
734 // like arm64).
735 size_t n = numRings_.load(std::memory_order_acquire);
736 for (size_t i = 0; i < n; ++i) {
737 size_t idx = (startRing + i) % n;
738 if (rings_[idx].try_pop(task)) {
739 startRing = idx;
740 executeNext(std::move(task));
741 return true;
742 }
743 }
744 startRing = 0;
745 return false;
746}
747
748inline void ThreadPool::executeNext(OnceFunction next) {
749 next();
750 workRemaining_.fetch_add(-1, std::memory_order_relaxed);
751}
752
753DISPENSO_INLINE bool ThreadPool::tryFindAndExecuteWork(
754 Ring& myRing,
755 StealRing& myStealRing,
756 size_t myStealIdx,
757 moodycamel::ConsumerToken& ctoken,
758 bool& preferRing,
759 int failCount,
760 bool checkQueue) {
761 OnceFunction task;
762 if (preferRing) {
763 bool fromRing = myRing.try_pop(task);
764 if (fromRing) {
765 task();
766 return true;
767 }
768 if (checkQueue && centralQueueNonEmpty_.load(std::memory_order_relaxed)) {
769 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
770 bool got = work_.try_dequeue(ctoken, task);
771 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
772 if (got) {
773 preferRing = false;
774 task();
775 return true;
776 }
777 // Empty on observation; clear flag (relaxed, plain store).
778 centralQueueNonEmpty_.store(false, std::memory_order_relaxed);
779 }
780 if (!myStealRing.empty() && myStealRing.try_pop(task)) {
781 task();
782 return true;
783 }
784 if (failCount >= kCrossRingFailThreshold) {
785 uint64_t mask = stealRingsWithWork_.load(std::memory_order_acquire);
786 if (mask != 0) {
787 if (myStealIdx < kMaxStealRings) {
788 mask &= ~(uint64_t{1} << myStealIdx);
789 }
790 if (mask != 0) {
791 int target = detail::countTrailingZeros(mask);
792 if (stealRings_[static_cast<size_t>(target)].try_pop(task)) {
793 task();
794 return true;
795 }
796 stealRingsWithWork_.fetch_and(~(uint64_t{1} << target), std::memory_order_relaxed);
797 }
798 }
799 }
800 } else {
801 if (checkQueue && centralQueueNonEmpty_.load(std::memory_order_relaxed)) {
802 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
803 bool got = work_.try_dequeue(ctoken, task);
804 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
805 if (got) {
806 task();
807 return true;
808 }
809 centralQueueNonEmpty_.store(false, std::memory_order_relaxed);
810 }
811 bool fromRing = myRing.try_pop(task);
812 if (fromRing) {
813 preferRing = true;
814 task();
815 return true;
816 }
817 }
818
819 return false;
820}
821
822template <typename Generator>
823DISPENSO_INLINE void ThreadPool::scheduleBulkToRingsFastPath(
824 size_t count,
825 size_t ringCount,
826 Generator&& gen,
827 moodycamel::ProducerToken* fallbackToken) {
828#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
829 // Pattern C cascade: wrap each cascade-host thread's task in a lambda
830 // that wakes its target group BEFORE running user work. Producer issues
831 // one wake-all on the seed group; the woken threads cascade in parallel
832 // to all other groups, then run their own user work.
833 auto* wsCascade = detail::consumeLoad(wakeState_);
834 bool useCascade = enableEpochWaiter_.load(std::memory_order_acquire) && wsCascade &&
835 wsCascade->totalSleeping() > 0;
836 for (size_t ring = 0; ring < count && ring < ringCount; ++ring) {
837 OnceFunction task = gen(ring);
838 int32_t target = useCascade
839 ? wsCascade->cascadeTargetFor(static_cast<int32_t>(ring), static_cast<int32_t>(count))
840 : -1;
841 if (target >= 0) {
842 OnceFunction wrapped = [wsCascade, target, inner = std::move(task)]() mutable {
843 wsCascade->cascadeWake(target);
844 inner();
845 };
846 if (!rings_[ring].try_push(std::move(wrapped))) {
847 enqueueToCentralQueue(std::move(wrapped), fallbackToken);
848 }
849 } else {
850 if (!rings_[ring].try_push(std::move(task))) {
851 enqueueToCentralQueue(std::move(task), fallbackToken);
852 }
853 }
854 }
855#else
856 for (size_t ring = 0; ring < count && ring < ringCount; ++ring) {
857 OnceFunction task = gen(ring);
858 if (!rings_[ring].try_push(std::move(task))) {
859 enqueueToCentralQueue(std::move(task), fallbackToken);
860 }
861 }
862#endif
863}
864
865template <typename Generator>
866DISPENSO_INLINE void ThreadPool::scheduleBulkToRingsBatched(
867 size_t count,
868 size_t ringCount,
869 size_t tasksPerRing,
870 Generator&& gen,
871 moodycamel::ProducerToken* fallbackToken) {
872 constexpr size_t kMaxStage = Ring::capacity();
873 size_t taskIdx = 0;
874 for (size_t ring = 0; ring < ringCount && taskIdx < count; ++ring) {
875 size_t blockEnd = std::min(taskIdx + tasksPerRing, count);
876 size_t blockSize = blockEnd - taskIdx;
877
878 size_t toStage = std::min(blockSize, kMaxStage);
879 OnceFunction staged[kMaxStage];
880 for (size_t j = 0; j < toStage; ++j) {
881 staged[j] = gen(taskIdx + j);
882 }
883
884 size_t pushed = rings_[ring].try_push_batch(staged, toStage);
885
886 for (size_t j = pushed; j < toStage; ++j) {
887 enqueueToCentralQueue(std::move(staged[j]), fallbackToken);
888 }
889
890 for (size_t j = taskIdx + toStage; j < blockEnd; ++j) {
891 enqueueToCentralQueue(gen(j), fallbackToken);
892 }
893 taskIdx += blockSize;
894 }
895}
896
897template <typename Generator>
898void ThreadPool::scheduleBulkToRings(
899 size_t count,
900 Generator&& gen,
901 moodycamel::ProducerToken* fallbackToken) {
902 if (count == 0) {
903 return;
904 }
905 assert(count <= numRings_.load(std::memory_order_relaxed));
906
907 workRemaining_.fetch_add(static_cast<ssize_t>(count), std::memory_order_release);
908
909 // Acquire: see tryExecuteNextFromRings. Pairs with the release store in
910 // resizeLocked so we observe the freshly-constructed rings, not merely the
911 // updated count.
912 size_t ringCount = numRings_.load(std::memory_order_acquire);
913 size_t tasksPerRing = (count + ringCount - 1) / ringCount;
914
915 if (tasksPerRing <= 1) {
916 scheduleBulkToRingsFastPath(count, ringCount, std::forward<Generator>(gen), fallbackToken);
917 } else {
918 scheduleBulkToRingsBatched(
919 count, ringCount, tasksPerRing, std::forward<Generator>(gen), fallbackToken);
920 }
921
922 auto* ws = detail::consumeLoad(wakeState_);
923 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
924#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
925 ws->cascadeWakeSeed(static_cast<int32_t>(count));
926#else
927 ws->wakeRange(static_cast<int32_t>(count));
928#endif
929 }
930}
931
932namespace detail {
933// Generating iterator for scheduleBulkEnqueue. Produces OnceFunction objects
934// on-the-fly during enqueue_bulk, avoiding the need for a staging buffer.
935// moodycamel's enqueue_bulk uses single-pass input iterator semantics.
936template <typename Generator>
937struct BulkGenIter {
938 using difference_type = std::ptrdiff_t;
939 using value_type = OnceFunction;
940 using pointer = OnceFunction*;
941 using reference = OnceFunction&;
942 using iterator_category = std::input_iterator_tag;
943
944 Generator* gen;
945 size_t index;
946 OnceFunction operator*() {
947 return (*gen)(index);
948 }
949 BulkGenIter& operator++() {
950 ++index;
951 return *this;
952 }
953 BulkGenIter operator++(int) {
954 BulkGenIter tmp = *this;
955 ++index;
956 return tmp;
957 }
958};
959} // namespace detail
960
961template <typename Generator>
962void ThreadPool::scheduleBulkEnqueue(
963 size_t count,
964 Generator&& gen,
965 moodycamel::ProducerToken* token) {
966 detail::BulkGenIter<typename std::remove_reference<Generator>::type> it{&gen, 0};
967
968 // Single atomic update + bulk enqueue
969 workRemaining_.fetch_add(static_cast<ssize_t>(count), std::memory_order_release);
970
971 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
972 bool enqueued;
973 // A token whose producer could not be allocated is invalid; enqueueing through it would
974 // dereference a null producer. Checking also tells GCC the producer is non-null, which
975 // otherwise reports -Wstringop-overflow on that path inside moodycamel.
976 if (token && DISPENSO_EXPECT(token->valid(), true)) {
977 enqueued = work_.enqueue_bulk(*token, it, count);
978 } else {
979 enqueued = work_.enqueue_bulk(it, count);
980 }
981 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
982 if (DISPENSO_EXPECT(!enqueued, false)) {
983 workRemaining_.fetch_sub(static_cast<ssize_t>(count), std::memory_order_relaxed);
984#if defined(__cpp_exceptions)
985 throw std::bad_alloc();
986#else
987 std::abort();
988#endif
989 }
990 // Mark queue as possibly-non-empty so spinning workers will try_dequeue.
991 centralQueueNonEmpty_.store(true, std::memory_order_relaxed);
992
993 // Wake appropriate threads. Cap by actual sleeping count to avoid over-waking.
994 // Spinning threads (numNotWorking - totalSleeping) will find enqueued work
995 // naturally, so only wake enough sleepers to cover the deficit beyond the
996 // spinner threshold.
997 auto* ws = detail::consumeLoad(wakeState_);
998 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
999 int32_t sleeping = ws->totalSleeping();
1000 if (sleeping > 0) {
1001 int32_t notWorking = numNotWorking_.load(std::memory_order_relaxed);
1002 int32_t spinning = std::max(int32_t{0}, notWorking - sleeping);
1003 // Only count spinners beyond the threshold as "covering" tasks
1004 int32_t effectiveSpinners = std::max(int32_t{0}, spinning - kSpinnerWakeThreshold + 1);
1005 int32_t toWake = std::max(int32_t{0}, static_cast<int32_t>(count) - effectiveSpinners);
1006 toWake = std::min(toWake, sleeping);
1007 if (toWake <= ws->branchFactor()) {
1008 // Small N: direct claim+wake, no cascade overhead.
1009 for (int32_t i = 0; i < toWake; ++i) {
1010 if (ws->claimAndWakeOne() < 0) {
1011 break;
1012 }
1013 }
1014 } else {
1015 // Large N: cascadeWakeSeed bumps and wakes every affected group
1016 // directly. Central-queue work is found by the standard
1017 // tryFindAndExecuteWork loop on each woken thread — no per-thread ring
1018 // pre-staging needed.
1019 ws->cascadeWakeSeed(toWake);
1020 }
1021 }
1022 }
1023}
1024
1025template <bool kPlaced, typename Generator>
1026void ThreadPool::scheduleBulkImpl(size_t count, Generator&& gen) {
1027 if (count == 0) {
1028 return;
1029 }
1030
1031 ssize_t numPool = numThreads_.load(std::memory_order_relaxed);
1032 if (!numPool) {
1033 for (size_t i = 0; i < count; ++i) {
1034 gen(i)();
1035 }
1036 return;
1037 }
1038
1039 // Process in chunks, interleaving enqueue and inline execution based on load.
1040 size_t chunkSize = static_cast<size_t>(numPool) + static_cast<size_t>(numPool) / 2;
1041 size_t i = 0;
1042 while (i < count) {
1043 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
1044 ssize_t loadFactor = poolLoadFactor_.load(std::memory_order_relaxed);
1045 if (curWork > loadFactor) {
1046 gen(i)();
1047 ++i;
1048 } else {
1049 ssize_t room = loadFactor - curWork;
1050 size_t toEnqueue = std::min({count - i, chunkSize, static_cast<size_t>(room)});
1051 if (toEnqueue == 0) {
1052 toEnqueue = 1;
1053 }
1054 size_t base = i;
1055 if (kPlaced) {
1056 workRemaining_.fetch_add(static_cast<ssize_t>(toEnqueue), std::memory_order_release);
1057 for (size_t j = 0; j < toEnqueue; ++j) {
1058 scheduleImplPlaced({gen(base + j)}, nullptr);
1059 }
1060 } else {
1061 scheduleBulkEnqueue(toEnqueue, [&gen, base](size_t j) { return gen(base + j); });
1062 }
1063 i += toEnqueue;
1064 }
1065 }
1066}
1067
1068template <typename Generator>
1069void ThreadPool::scheduleBulk(size_t count, Generator&& gen) {
1070 scheduleBulkImpl<false>(count, std::forward<Generator>(gen));
1071}
1072
1073template <typename Generator>
1074void ThreadPool::scheduleBulkPlaced(size_t count, Generator&& gen) {
1075 scheduleBulkImpl<true>(count, std::forward<Generator>(gen));
1076}
1077
1078} // 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:141
DISPENSO_DLL_ACCESS ThreadPool & globalThreadPool()
DISPENSO_DLL_ACCESS void resizeGlobalThreadPool(size_t numThreads)