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(__MACH__) || 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);
319 void conditionallyWake() {
320 auto* ws = detail::consumeLoad(wakeState_);
321 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
322 int32_t sleeping = ws->totalSleeping();
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();
336#if __cplusplus < 201703L
337 static void*
operator new(
size_t sz) {
338 return detail::alignedMalloc(sz);
340 static void operator delete(
void* ptr) {
341 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_;
450 std::deque<PerThreadData> threads_;
451 size_t poolLoadMultiplier_;
456 std::atomic<ssize_t> numThreads_;
458 moodycamel::ConcurrentQueue<OnceFunction> work_;
472 alignas(
kCacheLineSize) std::atomic<bool> centralQueueNonEmpty_{
false};
476 alignas(
kCacheLineSize) std::atomic<bool> enableEpochWaiter_{kDefaultWakeupEnable};
477 std::atomic<uint32_t> sleepLengthUs_{kDefaultSleepLenUs};
500 std::atomic<detail::PoolWakeState*> wakeState_{
nullptr};
501 std::vector<decltype(detail::makeAligned<detail::PoolWakeState>(0))> wakeStateGraveyard_;
506 ConcurrentObjectArena<Ring> rings_;
507 std::atomic<size_t> numRings_{0};
515 ConcurrentObjectArena<StealRing> stealRings_;
516 std::atomic<size_t> numStealRings_{0};
517 size_t stealRingSharing_{kStealRingSharing};
529 static constexpr size_t kMaxStealRings = 64;
530 alignas(
kCacheLineSize) std::atomic<uint64_t> stealRingsWithWork_{0};
539#if defined DISPENSO_DEBUG
540 alignas(
kCacheLineSize) std::atomic<ssize_t> outstandingTaskSets_{0};
543 friend class ConcurrentTaskSet;
544 friend class TaskSet;
545 friend class TaskSetBase;
547 template <
typename Result>
548 friend class detail::FutureBase;
549 template <
typename Result>
550 friend class detail::FutureImplBase;
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));
577template <
bool kPlaced,
typename F>
578inline void ThreadPool::forceEnqueue(F&& f, moodycamel::ProducerToken* token) {
579 if (!numThreads_.load(std::memory_order_relaxed)) {
583 workRemaining_.fetch_add(1, std::memory_order_release);
585 scheduleImplPlaced({std::forward<F>(f)}, token);
587 scheduleImpl({std::forward<F>(f)}, token);
592DISPENSO_REQUIRES(OnceCallableFunc<F>)
594 if (shouldRunInline()) {
597 schedule(std::forward<F>(f), ForceQueuingTag());
602DISPENSO_REQUIRES(OnceCallableFunc<F>)
605 static_cast<moodycamel::ProducerToken*
>(detail::PerPoolPerThreadInfo::producer(
this));
606 forceEnqueue<false>(std::forward<F>(f), token);
611 if (shouldRunInline()) {
614 schedule(token, std::forward<F>(f), ForceQueuingTag());
620 forceEnqueue<false>(std::forward<F>(f), &token);
624DISPENSO_REQUIRES(OnceCallableFunc<F>)
625inline void ThreadPool::schedulePlaced(F&& f) {
626 if (shouldRunInline()) {
629 schedulePlaced(std::forward<F>(f), ForceQueuingTag());
634DISPENSO_REQUIRES(OnceCallableFunc<F>)
635inline void ThreadPool::schedulePlaced(F&& f, ForceQueuingTag) {
637 static_cast<moodycamel::ProducerToken*
>(detail::PerPoolPerThreadInfo::producer(
this));
638 forceEnqueue<true>(std::forward<F>(f), token);
642inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f) {
643 if (shouldRunInline()) {
646 schedulePlaced(token, std::forward<F>(f), ForceQueuingTag());
651inline void ThreadPool::schedulePlaced(moodycamel::ProducerToken& token, F&& f, ForceQueuingTag) {
652 forceEnqueue<true>(std::forward<F>(f), &token);
655DISPENSO_INLINE
void ThreadPool::scheduleImpl(OnceFunction task, moodycamel::ProducerToken* token) {
656 enqueueToCentralQueue(std::move(task), token);
663 auto* ws = detail::consumeLoad(wakeState_);
664 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
665 int32_t sleeping = ws->totalSleeping();
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();
677DISPENSO_INLINE
void ThreadPool::scheduleImplPlaced(
679 moodycamel::ProducerToken* token) {
681 auto* ws = detail::consumeLoad(wakeState_);
682 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
683 int32_t sleeping = ws->totalSleeping();
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);
701 enqueueToCentralQueue(std::move(task), token);
706inline bool ThreadPool::tryExecuteNext() {
708 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
709 bool dequeued = work_.try_dequeue(next);
710 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_END();
712 executeNext(std::move(next));
718inline bool ThreadPool::tryExecuteNextFromProducerToken(moodycamel::ProducerToken& token) {
720 if (work_.try_dequeue_from_producer(token, next)) {
721 executeNext(std::move(next));
727inline bool ThreadPool::tryExecuteNextFromRings(
size_t& startRing) {
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)) {
739 executeNext(std::move(task));
747inline void ThreadPool::executeNext(OnceFunction next) {
749 workRemaining_.fetch_add(-1, std::memory_order_relaxed);
752DISPENSO_INLINE
bool ThreadPool::tryFindAndExecuteWork(
754 StealRing& myStealRing,
756 moodycamel::ConsumerToken& ctoken,
762 bool fromRing = myRing.try_pop(task);
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();
777 centralQueueNonEmpty_.store(
false, std::memory_order_relaxed);
779 if (!myStealRing.empty() && myStealRing.try_pop(task)) {
783 if (failCount >= kCrossRingFailThreshold) {
784 uint64_t mask = stealRingsWithWork_.load(std::memory_order_acquire);
786 if (myStealIdx < kMaxStealRings) {
787 mask &= ~(uint64_t{1} << myStealIdx);
790 int target = detail::countTrailingZeros(mask);
791 if (stealRings_[
static_cast<size_t>(target)].try_pop(task)) {
795 stealRingsWithWork_.fetch_and(~(uint64_t{1} << target), std::memory_order_relaxed);
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();
808 centralQueueNonEmpty_.store(
false, std::memory_order_relaxed);
810 bool fromRing = myRing.try_pop(task);
821template <
typename Generator>
822DISPENSO_INLINE
void ThreadPool::scheduleBulkToRingsFastPath(
826 moodycamel::ProducerToken* fallbackToken) {
827#if !defined(DISPENSO_DISABLE_CASCADE_WAKERANGE)
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))
841 OnceFunction wrapped = [wsCascade, target, inner = std::move(task)]()
mutable {
842 wsCascade->cascadeWake(target);
845 if (!rings_[ring].try_push(std::move(wrapped))) {
846 enqueueToCentralQueue(std::move(wrapped), fallbackToken);
849 if (!rings_[ring].try_push(std::move(task))) {
850 enqueueToCentralQueue(std::move(task), fallbackToken);
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);
864template <
typename Generator>
865DISPENSO_INLINE
void ThreadPool::scheduleBulkToRingsBatched(
870 moodycamel::ProducerToken* fallbackToken) {
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;
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);
883 size_t pushed = rings_[ring].try_push_batch(staged, toStage);
885 for (
size_t j = pushed; j < toStage; ++j) {
886 enqueueToCentralQueue(std::move(staged[j]), fallbackToken);
889 for (
size_t j = taskIdx + toStage; j < blockEnd; ++j) {
890 enqueueToCentralQueue(gen(j), fallbackToken);
892 taskIdx += blockSize;
896template <
typename Generator>
897void ThreadPool::scheduleBulkToRings(
900 moodycamel::ProducerToken* fallbackToken) {
904 assert(count <= numRings_.load(std::memory_order_relaxed));
906 workRemaining_.fetch_add(
static_cast<ssize_t
>(count), std::memory_order_release);
911 size_t ringCount = numRings_.load(std::memory_order_acquire);
912 size_t tasksPerRing = (count + ringCount - 1) / ringCount;
914 if (tasksPerRing <= 1) {
915 scheduleBulkToRingsFastPath(count, ringCount, std::forward<Generator>(gen), fallbackToken);
917 scheduleBulkToRingsBatched(
918 count, ringCount, tasksPerRing, std::forward<Generator>(gen), fallbackToken);
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));
926 ws->wakeRange(
static_cast<int32_t
>(count));
935template <
typename Generator>
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;
945 OnceFunction operator*() {
946 return (*gen)(index);
948 BulkGenIter& operator++() {
952 BulkGenIter operator++(
int) {
953 BulkGenIter tmp = *
this;
960template <
typename Generator>
961void ThreadPool::scheduleBulkEnqueue(
964 moodycamel::ProducerToken* token) {
965 detail::BulkGenIter<typename std::remove_reference<Generator>::type> it{&gen, 0};
968 workRemaining_.fetch_add(
static_cast<ssize_t
>(count), std::memory_order_release);
970 DISPENSO_TSAN_ANNOTATE_IGNORE_WRITES_BEGIN();
973 enqueued = work_.enqueue_bulk(*token, it, count);
975 enqueued = work_.enqueue_bulk(it, count);
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();
987 centralQueueNonEmpty_.store(
true, std::memory_order_relaxed);
993 auto* ws = detail::consumeLoad(wakeState_);
994 if (enableEpochWaiter_.load(std::memory_order_acquire) && ws) {
995 int32_t sleeping = ws->totalSleeping();
997 int32_t notWorking = numNotWorking_.load(std::memory_order_relaxed);
998 int32_t spinning = std::max(int32_t{0}, notWorking - sleeping);
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()) {
1005 for (int32_t i = 0; i < toWake; ++i) {
1006 if (ws->claimAndWakeOne() < 0) {
1015 ws->cascadeWakeSeed(toWake);
1021template <
bool kPlaced,
typename Generator>
1022void ThreadPool::scheduleBulkImpl(
size_t count, Generator&& gen) {
1027 ssize_t numPool = numThreads_.load(std::memory_order_relaxed);
1029 for (
size_t i = 0; i < count; ++i) {
1036 size_t chunkSize =
static_cast<size_t>(numPool) +
static_cast<size_t>(numPool) / 2;
1039 ssize_t curWork = workRemaining_.load(std::memory_order_relaxed);
1040 ssize_t loadFactor = poolLoadFactor_.load(std::memory_order_relaxed);
1041 if (curWork > loadFactor) {
1045 ssize_t room = loadFactor - curWork;
1046 size_t toEnqueue = std::min({count - i, chunkSize,
static_cast<size_t>(room)});
1047 if (toEnqueue == 0) {
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);
1057 scheduleBulkEnqueue(toEnqueue, [&gen, base](
size_t j) {
return gen(base + j); });
1064template <
typename Generator>
1066 scheduleBulkImpl<false>(count, std::forward<Generator>(gen));
1069template <
typename Generator>
1070void ThreadPool::scheduleBulkPlaced(
size_t count, Generator&& gen) {
1071 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)