19#include <condition_variable>
26#include <moodycamel/concurrentqueue.h>
30#include <dispenso/detail/math.h>
31#include <dispenso/detail/per_thread_info.h>
32#include <dispenso/detail/thread_pool_wake.h>
49T* consumeLoad(std::atomic<T*>& ptr) {
50 T* p = ptr.load(std::memory_order_relaxed);
51 DISPENSO_TSAN_ANNOTATE_HAPPENS_AFTER(&ptr);
57template <
typename Result>
59template <
typename Result>
63#if !defined(DISPENSO_WAKEUP_ENABLE)
64#if defined(_WIN32) || defined(__linux__) || defined(__APPLE__) || defined(__FreeBSD__)
65#define DISPENSO_WAKEUP_ENABLE 1
67#define DISPENSO_WAKEUP_ENABLE 0
74#if !defined(DISPENSO_POLL_PERIOD_US)
76#define DISPENSO_POLL_PERIOD_US 1000
78#define DISPENSO_POLL_PERIOD_US 200
87#if !defined(DISPENSO_WAKE_BACKSTOP_US)
88#define DISPENSO_WAKE_BACKSTOP_US 100000
91constexpr bool kDefaultWakeupEnable = DISPENSO_WAKEUP_ENABLE;
93constexpr uint32_t kDefaultSleepLenUs =
94 kDefaultWakeupEnable ? DISPENSO_WAKE_BACKSTOP_US : DISPENSO_POLL_PERIOD_US;
101struct ForceQueuingTag {};
117 DISPENSO_DLL_ACCESS
ThreadPool(
size_t n,
size_t poolLoadMultiplier = 32);
137 template <
class Rep,
class Period>
140 const std::chrono::duration<Rep, Period>& sleepDuration =
141 std::chrono::microseconds(kDefaultSleepLenUs)) {
144 static_cast<uint32_t
>(
145 std::chrono::duration_cast<std::chrono::microseconds>(sleepDuration).count()));
154 DISPENSO_DLL_ACCESS
void resize(ssize_t n) DISPENSO_NO_THREAD_SAFETY_ANALYSIS {
155 std::lock_guard<std::mutex> lk(threadsMutex_);
166 return numThreads_.load(std::memory_order_relaxed);
177 template <
typename F>
178 DISPENSO_REQUIRES(OnceCallableFunc<F>)
179 void schedule(F&& f);
189 template <
typename F>
190 DISPENSO_REQUIRES(OnceCallableFunc<F>)
191 void schedule(F&& f, ForceQueuingTag);
205 template <
typename Generator>
206 void scheduleBulk(
size_t count, Generator&& gen);
216 class PerThreadData {
218 void setThread(std::thread&& t);
227 std::atomic<bool> running_{
true};
230 DISPENSO_DLL_ACCESS uint32_t waitOnThread(int32_t threadIdx, uint32_t priorEpoch);
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();
236 enableEpochWaiter_.store(enable, std::memory_order_release);
237 sleepLengthUs_.store(sleepDurationUs, std::memory_order_release);
238 resizeLocked(currentPoolSize);
241 DISPENSO_DLL_ACCESS
void resizeLocked(ssize_t n);
243 void executeNext(OnceFunction work);
245 template <
bool kUseWakeSleep>
246 void threadLoopImpl(PerThreadData& threadData, int32_t ringIndex);
248 void threadLoopWake(PerThreadData& threadData, int32_t ringIndex) {
249 threadLoopImpl<true>(threadData, ringIndex);
251 void threadLoopPoll(PerThreadData& threadData, int32_t ringIndex) {
252 threadLoopImpl<false>(threadData, ringIndex);
255 void markWorkDone(
bool& isWorking);
256 void markIdle(
bool& isWorking);
258 bool tryExecuteNext();
259 bool tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token);
260 bool tryExecuteNextFromRings(
size_t& startRing);
263 DISPENSO_INLINE
bool shouldRunInline();
266 DISPENSO_INLINE
void scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token);
270 DISPENSO_INLINE
void scheduleImplPlaced(OnceFunction task, moodycamel::ProducerToken* token);
274 template <
bool kPlaced,
typename F>
275 inline void forceEnqueue(F&& f, moodycamel::ProducerToken* token);
277 template <
typename F>
278 void schedule(moodycamel::ProducerToken& token, F&& f);
280 template <
typename F>
281 void schedule(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag);
283 template <
typename F>
284 void schedulePlaced(moodycamel::ProducerToken& token, F&& f);
286 template <
typename F>
287 void schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag);
290 template <
typename F>
291 DISPENSO_REQUIRES(OnceCallableFunc<F>)
292 void schedulePlaced(F&& f);
294 template <
typename F>
295 DISPENSO_REQUIRES(OnceCallableFunc<F>)
296 void schedulePlaced(F&& f, ForceQueuingTag);
300 template <
bool kPlaced,
typename Generator>
301 void scheduleBulkImpl(
size_t count, Generator&& gen);
305 template <
typename Generator>
306 void scheduleBulkPlaced(
size_t count, Generator&& gen);
311 template <
typename Generator>
313 scheduleBulkEnqueue(
size_t count, Generator&& gen, moodycamel::ProducerToken* token =
nullptr);
318 void conditionallyWake() {
319 auto* ws = detail::consumeLoad(wakeState_);
320 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
321 int32_t sleeping = ws->totalSleeping();
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();
335#if DISPENSO_CPLUSPLUS < 201703L
336 static void*
operator new(
size_t sz) {
337 return detail::alignedMallocOrThrow(sz);
339 static void operator delete(
void* ptr) {
340 return detail::alignedFree(ptr);
348 using Ring = MpmcRingBuffer<OnceFunction, 16>;
352 static constexpr size_t kStealSlotsPerThread = 4;
354#if defined(DISPENSO_TUNE_STEAL_RING_SHARING)
355 static constexpr size_t kStealRingSharing = DISPENSO_TUNE_STEAL_RING_SHARING;
358 static constexpr size_t kStealRingSharing = 8;
360 static constexpr size_t kStealRingCapacity = kStealSlotsPerThread * kStealRingSharing;
361 using StealRing = MpmcRingBuffer<OnceFunction, kStealRingCapacity>;
368#if defined(DISPENSO_TUNE_CROSS_RING_FAIL_THRESHOLD)
369 static constexpr int kCrossRingFailThreshold = DISPENSO_TUNE_CROSS_RING_FAIL_THRESHOLD;
371 static constexpr int kCrossRingFailThreshold = 32;
381 DISPENSO_INLINE
void enqueueToCentralQueue(OnceFunction task, moodycamel::ProducerToken* token) {
382 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
385 enqueued = work_.enqueue(*token, std::move(task));
387 enqueued = work_.enqueue(std::move(task));
389 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
390 if (DISPENSO_EXPECT(!enqueued,
false)) {
391#if defined(__cpp_exceptions)
392 throw std::bad_alloc();
398 centralQueueNonEmpty_.store(
true, std::memory_order_relaxed);
405 template <
typename Generator>
406 void scheduleBulkToRings(
size_t count, Generator&& gen, moodycamel::ProducerToken* fallbackToken);
408 template <
typename Generator>
409 DISPENSO_INLINE
void scheduleBulkToRingsFastPath(
413 moodycamel::ProducerToken* fallbackToken);
415 template <
typename Generator>
416 DISPENSO_INLINE
void scheduleBulkToRingsBatched(
421 moodycamel::ProducerToken* fallbackToken);
429 DISPENSO_INLINE
bool tryFindAndExecuteWork(
431 StealRing& myStealRing,
433 moodycamel::ConsumerToken& ctoken,
436 bool checkQueue =
true);
442 static constexpr int kWorkBatchSize = 8;
447 static constexpr int32_t kSpinnerWakeThreshold = 2;
449 mutable std::mutex threadsMutex_;
451 std::deque<PerThreadData, detail::AlignedAllocator<PerThreadData>> threads_;
452 size_t poolLoadMultiplier_;
457 std::atomic<ssize_t> numThreads_;
459 moodycamel::ConcurrentQueue<OnceFunction> work_;
473 alignas(
kCacheLineSize) std::atomic<bool> centralQueueNonEmpty_{
false};
477 alignas(
kCacheLineSize) std::atomic<bool> enableEpochWaiter_{kDefaultWakeupEnable};
478 std::atomic<uint32_t> sleepLengthUs_{kDefaultSleepLenUs};
501 std::atomic<detail::PoolWakeState*> wakeState_{
nullptr};
502 std::vector<decltype(detail::makeAligned<detail::PoolWakeState>(0))> wakeStateGraveyard_;
507 ConcurrentObjectArena<Ring> rings_;
508 std::atomic<size_t> numRings_{0};
516 ConcurrentObjectArena<StealRing> stealRings_;
517 std::atomic<size_t> numStealRings_{0};
518 size_t stealRingSharing_{kStealRingSharing};
530 static constexpr size_t kMaxStealRings = 64;
531 alignas(
kCacheLineSize) std::atomic<uint64_t> stealRingsWithWork_{0};
540#if defined DISPENSO_DEBUG
541 alignas(
kCacheLineSize) std::atomic<ssize_t> outstandingTaskSets_{0};
544 friend class ConcurrentTaskSet;
545 friend class TaskSet;
546 friend class TaskSetBase;
548 template <
typename Result>
549 friend class detail::FutureBase;
550 template <
typename Result>
551 friend class detail::FutureImplBase;
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));
578template <
bool kPlaced,
typename F>
579inline void ThreadPool::forceEnqueue(F&& f, moodycamel::ProducerToken* token) {
580 if (!numThreads_.load(std::memory_order_relaxed)) {
584 workRemaining_.fetch_add(1, std::memory_order_release);
586 scheduleImplPlaced({std::forward<F>(f)}, token);
588 scheduleImpl({std::forward<F>(f)}, token);
593DISPENSO_REQUIRES(OnceCallableFunc<F>)
595 if (shouldRunInline()) {
598 schedule(std::forward<F>(f), ForceQueuingTag());
603DISPENSO_REQUIRES(OnceCallableFunc<F>)
606 static_cast<moodycamel::ProducerToken*
>(detail::PerPoolPerThreadInfo::producer(
this));
607 forceEnqueue<false>(std::forward<F>(f), token);
612 if (shouldRunInline()) {
615 schedule(token, std::forward<F>(f), ForceQueuingTag());
621 forceEnqueue<false>(std::forward<F>(f), &token);
625DISPENSO_REQUIRES(OnceCallableFunc<F>)
626inline void ThreadPool::schedulePlaced(F&& f) {
627 if (shouldRunInline()) {
630 schedulePlaced(std::forward<F>(f), ForceQueuingTag());
635DISPENSO_REQUIRES(OnceCallableFunc<F>)
636inline void ThreadPool::schedulePlaced(F&& f, ForceQueuingTag) {
638 static_cast<moodycamel::ProducerToken*
>(detail::PerPoolPerThreadInfo::producer(
this));
639 forceEnqueue<true>(std::forward<F>(f), token);
643inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f) {
644 if (shouldRunInline()) {
647 schedulePlaced(token, std::forward<F>(f), ForceQueuingTag());
652inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
653 forceEnqueue<true>(std::forward<F>(f), &token);
656DISPENSO_INLINE
void ThreadPool::scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token) {
657 enqueueToCentralQueue(std::move(task), token);
664 auto* ws = detail::consumeLoad(wakeState_);
665 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
666 int32_t sleeping = ws->totalSleeping();
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();
678DISPENSO_INLINE
void ThreadPool::scheduleImplPlaced(
680 moodycamel::ProducerToken* token) {
682 auto* ws = detail::consumeLoad(wakeState_);
683 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
684 int32_t sleeping = ws->totalSleeping();
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);
702 enqueueToCentralQueue(std::move(task), token);
707inline bool ThreadPool::tryExecuteNext() {
709 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
710 bool dequeued = work_.try_dequeue(next);
711 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
713 executeNext(std::move(next));
719inline bool ThreadPool::tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token) {
721 if (work_.try_dequeue_from_producer(token, next)) {
722 executeNext(std::move(next));
728inline bool ThreadPool::tryExecuteNextFromRings(
size_t& startRing) {
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)) {
740 executeNext(std::move(task));
748inline void ThreadPool::executeNext(OnceFunction next) {
750 workRemaining_.fetch_add(-1, std::memory_order_relaxed);
753DISPENSO_INLINE
bool ThreadPool::tryFindAndExecuteWork(
755 StealRing& myStealRing,
757 moodycamel::ConsumerToken& ctoken,
763 bool fromRing = myRing.try_pop(task);
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();
778 centralQueueNonEmpty_.store(
false, std::memory_order_relaxed);
780 if (!myStealRing.empty() && myStealRing.try_pop(task)) {
784 if (failCount >= kCrossRingFailThreshold) {
785 uint64_t mask = stealRingsWithWork_.load(std::memory_order_acquire);
787 if (myStealIdx < kMaxStealRings) {
788 mask &= ~(uint64_t{1} << myStealIdx);
791 int target = detail::countTrailingZeros(mask);
792 if (stealRings_[
static_cast<size_t>(target)].try_pop(task)) {
796 stealRingsWithWork_.fetch_and(~(uint64_t{1} << target), std::memory_order_relaxed);
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();
809 centralQueueNonEmpty_.store(
false, std::memory_order_relaxed);
811 bool fromRing = myRing.try_pop(task);
822template <
typename Generator>
823DISPENSO_INLINE
void ThreadPool::scheduleBulkToRingsFastPath(
827 moodycamel::ProducerToken* fallbackToken) {
828#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
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))
842 OnceFunction wrapped = [wsCascade, target, inner = std::move(task)]()
mutable {
843 wsCascade->cascadeWake(target);
846 if (!rings_[ring].try_push(std::move(wrapped))) {
847 enqueueToCentralQueue(std::move(wrapped), fallbackToken);
850 if (!rings_[ring].try_push(std::move(task))) {
851 enqueueToCentralQueue(std::move(task), fallbackToken);
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);
865template <
typename Generator>
866DISPENSO_INLINE
void ThreadPool::scheduleBulkToRingsBatched(
871 moodycamel::ProducerToken* fallbackToken) {
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;
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);
884 size_t pushed = rings_[ring].try_push_batch(staged, toStage);
886 for (
size_t j = pushed; j < toStage; ++j) {
887 enqueueToCentralQueue(std::move(staged[j]), fallbackToken);
890 for (
size_t j = taskIdx + toStage; j < blockEnd; ++j) {
891 enqueueToCentralQueue(gen(j), fallbackToken);
893 taskIdx += blockSize;
897template <
typename Generator>
898void ThreadPool::scheduleBulkToRings(
901 moodycamel::ProducerToken* fallbackToken) {
905 assert(count <= numRings_.load(std::memory_order_relaxed));
907 workRemaining_.fetch_add(
static_cast<ssize_t
>(count), std::memory_order_release);
912 size_t ringCount = numRings_.load(std::memory_order_acquire);
913 size_t tasksPerRing = (count + ringCount - 1) / ringCount;
915 if (tasksPerRing <= 1) {
916 scheduleBulkToRingsFastPath(count, ringCount, std::forward<Generator>(gen), fallbackToken);
918 scheduleBulkToRingsBatched(
919 count, ringCount, tasksPerRing, std::forward<Generator>(gen), fallbackToken);
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));
927 ws->wakeRange(
static_cast<int32_t
>(count));
936template <
typename Generator>
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;
946 OnceFunction operator*() {
947 return (*gen)(index);
949 BulkGenIter& operator++() {
953 BulkGenIter operator++(
int) {
954 BulkGenIter tmp = *
this;
961template <
typename Generator>
962void ThreadPool::scheduleBulkEnqueue(
965 moodycamel::ProducerToken* token) {
966 detail::BulkGenIter<typename std::remove_reference<Generator>::type> it{&gen, 0};
969 workRemaining_.fetch_add(
static_cast<ssize_t
>(count), std::memory_order_release);
971 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
976 if (token && DISPENSO_EXPECT(token->valid(),
true)) {
977 enqueued = work_.enqueue_bulk(*token, it, count);
979 enqueued = work_.enqueue_bulk(it, count);
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();
991 centralQueueNonEmpty_.store(
true, std::memory_order_relaxed);
997 auto* ws = detail::consumeLoad(wakeState_);
998 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
999 int32_t sleeping = ws->totalSleeping();
1001 int32_t notWorking = numNotWorking_.load(std::memory_order_relaxed);
1002 int32_t spinning = std::max(int32_t{0}, notWorking - sleeping);
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()) {
1009 for (int32_t i = 0; i < toWake; ++i) {
1010 if (ws->claimAndWakeOne() < 0) {
1019 ws->cascadeWakeSeed(toWake);
1025template <
bool kPlaced,
typename Generator>
1026void ThreadPool::scheduleBulkImpl(
size_t count, Generator&& gen) {
1031 ssize_t numPool = numThreads_.load(std::memory_order_relaxed);
1033 for (
size_t i = 0; i < count; ++i) {
1040 size_t chunkSize =
static_cast<size_t>(numPool) +
static_cast<size_t>(numPool) / 2;
1043 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
1044 ssize_t loadFactor = poolLoadFactor_.load(std::memory_order_relaxed);
1045 if (curWork > loadFactor) {
1049 ssize_t room = loadFactor - curWork;
1050 size_t toEnqueue = std::min({count - i, chunkSize,
static_cast<size_t>(room)});
1051 if (toEnqueue == 0) {
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);
1061 scheduleBulkEnqueue(toEnqueue, [&gen, base](
size_t j) {
return gen(base + j); });
1068template <
typename Generator>
1070 scheduleBulkImpl<false>(count, std::forward<Generator>(gen));
1073template <
typename Generator>
1074void ThreadPool::scheduleBulkPlaced(
size_t count, Generator&& gen) {
1075 scheduleBulkImpl<true>(count, std::forward<Generator>(gen));
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)
DISPENSO_DLL_ACCESS ThreadPool & globalThreadPool()
DISPENSO_DLL_ACCESS void resizeGlobalThreadPool(size_t numThreads)