ThreadSchedule 2.2.0
Modern C++ thread management library
Loading...
Searching...
No Matches
thread_pool.hpp
Go to the documentation of this file.
1#pragma once
2
7
8#include "callable.hpp"
9#include "expected.hpp"
10#include "scheduler_policy.hpp"
11#include "thread_registry.hpp"
12#include "thread_wrapper.hpp"
13#include <algorithm>
14#include <array>
15#include <atomic>
16#include <condition_variable>
17#include <cstddef>
18#include <cstdint>
19#include <future>
20#include <mutex>
21#include <optional>
22#include <queue>
23#include <random>
24#include <type_traits>
25#include <tuple>
26#include <vector>
27
28#if __cpp_lib_ranges >= 201911L
29# include <ranges>
30#endif
31
32namespace threadschedule
33{
34
35namespace detail
36{
37
38template <typename WorkerRange>
39inline auto configure_worker_threads(WorkerRange& workers, std::string const& name_prefix, SchedulingPolicy policy,
41{
42 bool success = true;
43 for (size_t i = 0; i < workers.size(); ++i)
44 {
45 std::string const thread_name = name_prefix + "_" + std::to_string(i);
46 if (!workers[i].set_name(thread_name).has_value())
47 success = false;
48 if (!workers[i].set_scheduling_policy(policy, priority).has_value())
49 success = false;
50 }
51 if (success)
52 return {};
53 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
54}
55
56template <typename WorkerRange>
57inline auto set_worker_affinity(WorkerRange& workers, ThreadAffinity const& affinity) -> expected<void, std::error_code>
58{
59 bool success = true;
60 for (auto& worker : workers)
61 {
62 if (!worker.set_affinity(affinity).has_value())
63 success = false;
64 }
65 if (success)
66 return {};
67 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
68}
69
70template <typename WorkerRange>
72{
73 auto const cpu_count = std::thread::hardware_concurrency();
74 if (cpu_count == 0)
75 return unexpected(std::make_error_code(std::errc::invalid_argument));
76
77 bool success = true;
78 for (size_t i = 0; i < workers.size(); ++i)
79 {
80 ThreadAffinity affinity({static_cast<int>(i % cpu_count)});
81 if (!workers[i].set_affinity(affinity).has_value())
82 success = false;
83 }
84 if (success)
85 return {};
86 return unexpected(std::make_error_code(std::errc::operation_not_permitted));
87}
88
89template <typename Pool, typename Iterator, typename F>
90inline void parallel_for_each_chunked(Pool& pool, Iterator begin, Iterator end, F&& func, size_t num_workers)
91{
92 auto const total = static_cast<size_t>(std::distance(begin, end));
93 if (total == 0)
94 return;
95
96 size_t const chunk_size = (std::max)(size_t(1), total / (num_workers * 4));
97 std::vector<std::future<void>> futures;
98 auto it = begin;
99
100 while (it != end)
101 {
102 auto remaining = static_cast<size_t>(std::distance(it, end));
103 auto this_chunk = (std::min)(chunk_size, remaining);
104 auto chunk_end = it;
105 std::advance(chunk_end, this_chunk);
106
107 futures.push_back(pool.submit([it, chunk_end, &func]() {
108 for (auto cur = it; cur != chunk_end; ++cur)
109 func(*cur);
110 }));
111
112 it = chunk_end;
113 }
114
115 for (auto& f : futures)
116 f.get();
117}
118
119// ---------------------------------------------------------------------------
120// bind_args -- optimal argument binding, C++20 pack-capture or C++17 tuple
121// ---------------------------------------------------------------------------
122
130template <typename F, typename... Args>
131auto bind_args(F&& f, Args&&... args)
132{
133#if __cpp_init_captures >= 201803L
134 return [fn = std::forward<F>(f), ... a = std::forward<Args>(args)]() mutable { return fn(std::move(a)...); };
135#else
136 return [fn = std::forward<F>(f), tup = std::make_tuple(std::forward<Args>(args)...)]() mutable {
137 return std::apply(std::move(fn), std::move(tup));
138 };
139#endif
140}
141
142// ---------------------------------------------------------------------------
143// SboCallable -- type-erased callable with inline small-buffer storage
144// ---------------------------------------------------------------------------
145
179template <size_t TaskSize = 64>
181{
182 static_assert(TaskSize > sizeof(void*), "TaskSize must be larger than a pointer");
183
184 struct VTable
185 {
186 void (*invoke)(void* storage);
187 void (*destroy)(void* storage);
188 void (*move_to)(void* dst, void* src) noexcept;
189 };
190
191 static constexpr size_t kBufferSize = TaskSize - sizeof(VTable const*);
192
193 template <typename F>
194 static constexpr bool fits_inline_v =
195 sizeof(F) <= kBufferSize && alignof(F) <= alignof(std::max_align_t) && std::is_nothrow_move_constructible_v<F>;
196
197 template <typename F>
198 static VTable const* vtable_for() noexcept
199 {
200 if constexpr (fits_inline_v<F>)
201 {
202 static constexpr VTable vt{[](void* s) { (*static_cast<F*>(s))(); },
203 [](void* s) { static_cast<F*>(s)->~F(); },
204 [](void* dst, void* src) noexcept {
205 ::new (dst) F(std::move(*static_cast<F*>(src)));
206 static_cast<F*>(src)->~F();
207 }};
208 return &vt;
209 }
210 else
211 {
212 static constexpr VTable vt{[](void* s) { (*(*static_cast<F**>(s)))(); },
213 [](void* s) { delete *static_cast<F**>(s); },
214 [](void* dst, void* src) noexcept {
215 *static_cast<F**>(dst) = *static_cast<F**>(src);
216 *static_cast<F**>(src) = nullptr;
217 }};
218 return &vt;
219 }
220 }
221
222 public:
223 SboCallable() = default;
224
225 template <typename F, typename = std::enable_if_t<!std::is_same_v<std::decay_t<F>, SboCallable>>>
226 SboCallable(F&& f) // NOLINT(google-explicit-constructor)
227 {
228 using Decay = std::decay_t<F>;
229 vtable_ = vtable_for<Decay>();
230 if constexpr (fits_inline_v<Decay>)
231 ::new (buffer_) Decay(std::forward<F>(f));
232 else
233 *reinterpret_cast<Decay**>(buffer_) = new Decay(std::forward<F>(f));
234 }
235
236 SboCallable(SboCallable&& other) noexcept : vtable_(other.vtable_)
237 {
238 if (vtable_)
239 {
240 vtable_->move_to(buffer_, other.buffer_);
241 other.vtable_ = nullptr;
242 }
243 }
244
245 auto operator=(SboCallable&& other) noexcept -> SboCallable&
246 {
247 if (this != &other)
248 {
249 if (vtable_)
250 vtable_->destroy(buffer_);
251 vtable_ = other.vtable_;
252 if (vtable_)
253 {
254 vtable_->move_to(buffer_, other.buffer_);
255 other.vtable_ = nullptr;
256 }
257 }
258 return *this;
259 }
260
261 SboCallable(SboCallable const&) = delete;
262 auto operator=(SboCallable const&) -> SboCallable& = delete;
263
265 {
266 if (vtable_)
267 vtable_->destroy(buffer_);
268 }
269
270 explicit operator bool() const noexcept
271 {
272 return vtable_ != nullptr;
273 }
274
276 {
277 auto* vt = vtable_;
278 vtable_ = nullptr;
279 vt->invoke(buffer_);
280 vt->destroy(buffer_);
281 }
282
283 private:
284 VTable const* vtable_ = nullptr;
285 alignas(std::max_align_t) unsigned char buffer_[kBufferSize];
286};
287
288} // namespace detail
289
320
322using TaskStartCallback = detail::copyable_callable<void(std::chrono::steady_clock::time_point, std::thread::id)>;
323
325using TaskEndCallback = detail::copyable_callable<void(std::chrono::steady_clock::time_point, std::thread::id,
326 std::chrono::microseconds elapsed)>;
327
330
331template <typename T>
333{
334 public:
335 static constexpr size_t CACHE_LINE_SIZE = 64;
336 static constexpr size_t DEFAULT_CAPACITY = 1024;
337
338 private:
339 struct alignas(CACHE_LINE_SIZE) AlignedItem
340 {
341 T item;
342 AlignedItem() = default;
343 AlignedItem(T&& t) : item(std::move(t))
344 {
345 }
346 template <typename U = T, std::enable_if_t<std::is_copy_constructible_v<U>, int> = 0>
347 AlignedItem(T const& t) : item(t)
348 {
349 }
350 };
351
352 std::unique_ptr<AlignedItem[]> buffer_;
353 size_t capacity_;
354
355 alignas(CACHE_LINE_SIZE) std::atomic<size_t> top_{0}; // Owner pushes/pops here
356 alignas(CACHE_LINE_SIZE) std::atomic<size_t> bottom_{0}; // Thieves steal here
357 alignas(CACHE_LINE_SIZE) mutable std::mutex mutex_; // For synchronization
358
359 public:
360 explicit WorkStealingDeque(size_t capacity = DEFAULT_CAPACITY)
361 : buffer_(std::make_unique<AlignedItem[]>(capacity)), capacity_(capacity)
362 {
363 }
364
365 [[nodiscard]] auto push(T&& item) -> bool
366 {
367 std::lock_guard<std::mutex> lock(mutex_);
368 size_t const t = top_.load(std::memory_order_relaxed);
369 size_t const b = bottom_.load(std::memory_order_relaxed);
370
371 if (t - b >= capacity_)
372 {
373 return false;
374 }
375
376 buffer_[t % capacity_] = AlignedItem(std::move(item));
377 top_.store(t + 1, std::memory_order_release);
378 return true;
379 }
380
381 template <typename U = T, std::enable_if_t<std::is_copy_constructible_v<U>, int> = 0>
382 [[nodiscard]] auto push(T const& item) -> bool
383 {
384 std::lock_guard<std::mutex> lock(mutex_);
385 size_t const t = top_.load(std::memory_order_relaxed);
386 size_t const b = bottom_.load(std::memory_order_relaxed);
387
388 if (t - b >= capacity_)
389 {
390 return false;
391 }
392
393 buffer_[t % capacity_] = AlignedItem(item);
394 top_.store(t + 1, std::memory_order_release);
395 return true;
396 }
397
398 [[nodiscard]] auto pop(T& item) -> bool
399 {
400 std::lock_guard<std::mutex> lock(mutex_);
401 size_t const t = top_.load(std::memory_order_relaxed);
402 size_t const b = bottom_.load(std::memory_order_relaxed);
403
404 if (t <= b)
405 {
406 return false;
407 }
408
409 size_t const new_top = t - 1;
410 item = std::move(buffer_[new_top % capacity_].item);
411 top_.store(new_top, std::memory_order_relaxed);
412 return true;
413 }
414
415 [[nodiscard]] auto steal(T& item) -> bool
416 {
417 std::lock_guard<std::mutex> lock(mutex_);
418 size_t const b = bottom_.load(std::memory_order_relaxed);
419 size_t const t = top_.load(std::memory_order_relaxed);
420
421 if (b >= t)
422 {
423 return false;
424 }
425
426 item = std::move(buffer_[b % capacity_].item);
427 bottom_.store(b + 1, std::memory_order_relaxed);
428 return true;
429 }
430
431 [[nodiscard]] auto size() const -> size_t
432 {
433 size_t const t = top_.load(std::memory_order_relaxed);
434 size_t const b = bottom_.load(std::memory_order_relaxed);
435 return t > b ? t - b : 0;
436 }
437
438 [[nodiscard]] auto empty() const -> bool
439 {
440 return size() == 0;
441 }
442
443 void clear()
444 {
445 std::lock_guard<std::mutex> lock(mutex_);
446 bottom_.store(0, std::memory_order_relaxed);
447 top_.store(0, std::memory_order_relaxed);
448 }
449
450 [[nodiscard]] auto clear_and_count() -> size_t
451 {
452 std::lock_guard<std::mutex> lock(mutex_);
453 size_t const t = top_.load(std::memory_order_relaxed);
454 size_t const b = bottom_.load(std::memory_order_relaxed);
455 size_t const count = t > b ? t - b : 0;
456 bottom_.store(0, std::memory_order_relaxed);
457 top_.store(0, std::memory_order_relaxed);
458 return count;
459 }
460};
461
476enum class ShutdownPolicy : uint8_t
477{
480};
481
563{
564 public:
565 using Task = std::function<void()>;
567
569 {
576 std::chrono::microseconds avg_task_time;
577 };
578
579 explicit HighPerformancePool(size_t num_threads = std::thread::hardware_concurrency(),
581 bool register_workers = false)
582 : num_threads_(num_threads == 0 ? 1 : num_threads), register_workers_(register_workers), stop_(false),
583 next_victim_(0), start_time_(std::chrono::steady_clock::now())
584 {
585 worker_queues_.resize(num_threads_);
586 for (size_t i = 0; i < num_threads_; ++i)
587 {
588 worker_queues_[i] = std::make_unique<WorkStealingDeque<QueuedTask>>(deque_capacity);
589 }
590
591 workers_.reserve(num_threads_);
592
593 for (size_t i = 0; i < num_threads_; ++i)
594 {
595 workers_.emplace_back(&HighPerformancePool::worker_function, this, i);
596 }
597 }
598
601
606
614 {
615 size_t dropped_tasks = 0;
616 {
617 std::lock_guard<std::mutex> lock(overflow_mutex_);
618 if (stop_.exchange(true, std::memory_order_acq_rel))
619 return;
620
621 if (policy == ShutdownPolicy::drop_pending)
622 {
623 dropped_tasks += overflow_tasks_.size();
624 std::queue<QueuedTask> empty;
625 overflow_tasks_.swap(empty);
626 for (auto& q : worker_queues_)
627 dropped_tasks += q->clear_and_count();
628 }
629 }
630
631 if (dropped_tasks != 0)
632 {
633 std::lock_guard<std::mutex> lock(completion_mutex_);
634 outstanding_tasks_.fetch_sub(dropped_tasks, std::memory_order_acq_rel);
635 }
636
637 if (dropped_tasks != 0)
638 completion_condition_.notify_all();
639 wakeup_condition_.notify_all();
640
641 for (auto& worker : workers_)
642 {
643 if (worker.joinable())
644 worker.join();
645 }
646
647 workers_.clear();
648 }
649
656 auto shutdown_for(std::chrono::milliseconds timeout) -> bool
657 {
658 auto const deadline = std::chrono::steady_clock::now() + timeout;
659
660 {
661 std::lock_guard<std::mutex> lock(overflow_mutex_);
662 if (stop_.load(std::memory_order_acquire))
663 return true;
664 }
665
666 std::unique_lock<std::mutex> lock(completion_mutex_);
667 bool const drained = completion_condition_.wait_until(
668 lock, deadline, [this] { return outstanding_tasks_.load(std::memory_order_acquire) == 0; });
669
671 return drained;
672 }
673
690 template <typename F, typename... Args>
691 auto try_submit(F&& f, Args&&... args) -> expected<std::future<std::invoke_result_t<F, Args...>>, std::error_code>
692 {
693 using return_type = std::invoke_result_t<F, Args...>;
694
695 auto task = std::make_shared<std::packaged_task<return_type()>>(
696 detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
697
698 std::future<return_type> result = task->get_future();
699
700 if (stop_.load(std::memory_order_acquire))
701 return unexpected(std::make_error_code(std::errc::operation_canceled));
702
703 size_t const preferred_queue = next_victim_.fetch_add(1, std::memory_order_relaxed) % num_threads_;
704
705 outstanding_tasks_.fetch_add(1, std::memory_order_release);
706 if (worker_queues_[preferred_queue]->push([task]() { (*task)(); }))
707 {
708 wakeup_condition_.notify_one();
709 return result;
710 }
711 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
712
713 for (size_t attempts = 0; attempts < (std::min)(num_threads_, size_t(3)); ++attempts)
714 {
715 size_t const idx = (preferred_queue + attempts + 1) % num_threads_;
716 outstanding_tasks_.fetch_add(1, std::memory_order_release);
717 if (worker_queues_[idx]->push([task]() { (*task)(); }))
718 {
719 wakeup_condition_.notify_one();
720 return result;
721 }
722 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
723 }
724
725 {
726 std::lock_guard<std::mutex> lock(overflow_mutex_);
727 if (stop_.load(std::memory_order_relaxed))
728 return unexpected(std::make_error_code(std::errc::operation_canceled));
729 outstanding_tasks_.fetch_add(1, std::memory_order_release);
730 overflow_tasks_.emplace([task]() { (*task)(); });
731 }
732
733 wakeup_condition_.notify_all();
734 return result;
735 }
736
746 template <typename F, typename... Args>
747 auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>
748 {
749 auto result = try_submit(std::forward<F>(f), std::forward<Args>(args)...);
750 if (!result.has_value())
751 throw std::runtime_error("HighPerformancePool is shutting down");
752 return std::move(result.value());
753 }
754
765 template <typename F, typename... Args>
766 void post(F&& f, Args&&... args)
767 {
768 auto r = try_post(std::forward<F>(f), std::forward<Args>(args)...);
769 if (!r.has_value())
770 throw std::runtime_error("HighPerformancePool is shutting down");
771 }
772
779 template <typename F, typename... Args>
780 auto try_post(F&& f, Args&&... args) -> expected<void, std::error_code>
781 {
783 detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...)));
784
785 if (stop_.load(std::memory_order_acquire))
786 return unexpected(std::make_error_code(std::errc::operation_canceled));
787
788 size_t const preferred_queue = next_victim_.fetch_add(1, std::memory_order_relaxed) % num_threads_;
789
790 outstanding_tasks_.fetch_add(1, std::memory_order_release);
791 if (worker_queues_[preferred_queue]->push(std::move(bound)))
792 {
793 wakeup_condition_.notify_one();
794 return {};
795 }
796 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
797
798 for (size_t attempts = 0; attempts < (std::min)(num_threads_, size_t(3)); ++attempts)
799 {
800 size_t const idx = (preferred_queue + attempts + 1) % num_threads_;
801 outstanding_tasks_.fetch_add(1, std::memory_order_release);
802 if (worker_queues_[idx]->push(std::move(bound)))
803 {
804 wakeup_condition_.notify_one();
805 return {};
806 }
807 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
808 }
809
810 {
811 std::lock_guard<std::mutex> lock(overflow_mutex_);
812 if (stop_.load(std::memory_order_relaxed))
813 return unexpected(std::make_error_code(std::errc::operation_canceled));
814 outstanding_tasks_.fetch_add(1, std::memory_order_release);
815 overflow_tasks_.emplace(std::move(bound));
816 }
817
818 wakeup_condition_.notify_all();
819 return {};
820 }
821
822#if __cpp_lib_jthread >= 201911L
829 template <typename F, typename... Args>
830 auto submit(std::stop_token token, F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>
831 {
832 return submit([token = std::move(token),
833 bound = detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
834 if (token.stop_requested())
835 return std::invoke_result_t<F, Args...>();
836 return bound();
837 });
838 }
839
841 template <typename F, typename... Args>
842 auto try_submit(std::stop_token token, F&& f, Args&&... args)
843 -> expected<std::future<std::invoke_result_t<F, Args...>>, std::error_code>
844 {
845 return try_submit([token = std::move(token),
846 bound = detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
847 if (token.stop_requested())
848 return std::invoke_result_t<F, Args...>();
849 return bound();
850 });
851 }
852#endif
853
865 template <typename Iterator>
866 auto try_submit_batch(Iterator begin, Iterator end) -> expected<std::vector<std::future<void>>, std::error_code>
867 {
868 std::vector<std::future<void>> futures;
869 size_t const batch_size = std::distance(begin, end);
870 futures.reserve(batch_size);
871
872 if (stop_.load(std::memory_order_acquire))
873 return unexpected(std::make_error_code(std::errc::operation_canceled));
874
875 size_t queue_idx = next_victim_.fetch_add(batch_size, std::memory_order_relaxed) % num_threads_;
876
877 for (auto it = begin; it != end; ++it)
878 {
879 auto task = std::make_shared<std::packaged_task<void()>>(*it);
880 futures.push_back(task->get_future());
881
882 bool queued = false;
883 for (size_t attempts = 0; attempts < num_threads_; ++attempts)
884 {
885 outstanding_tasks_.fetch_add(1, std::memory_order_release);
886 if (worker_queues_[queue_idx]->push([task]() { (*task)(); }))
887 {
888 queued = true;
889 break;
890 }
891 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
892 queue_idx = (queue_idx + 1) % num_threads_;
893 }
894
895 if (!queued)
896 {
897 std::lock_guard<std::mutex> lock(overflow_mutex_);
898 outstanding_tasks_.fetch_add(1, std::memory_order_release);
899 overflow_tasks_.emplace([task]() { (*task)(); });
900 }
901 }
902
903 wakeup_condition_.notify_all();
904 return futures;
905 }
906
912 template <typename Iterator>
913 auto submit_batch(Iterator begin, Iterator end) -> std::vector<std::future<void>>
914 {
915 auto result = try_submit_batch(begin, end);
916 if (!result.has_value())
917 throw std::runtime_error("HighPerformancePool is shutting down");
918 return std::move(result.value());
919 }
920
927 template <typename Iterator, typename F>
928 void parallel_for_each(Iterator begin, Iterator end, F&& func)
929 {
930 detail::parallel_for_each_chunked(*this, begin, end, std::forward<F>(func), num_threads_);
931 }
932
933#if __cpp_lib_ranges >= 201911L
935 template <std::ranges::input_range R>
936 auto submit_batch(R&& range)
937 {
938 return submit_batch(std::ranges::begin(range), std::ranges::end(range));
939 }
940
941 template <std::ranges::input_range R>
942 auto try_submit_batch(R&& range)
943 {
944 return try_submit_batch(std::ranges::begin(range), std::ranges::end(range));
945 }
946
947 template <std::ranges::input_range R, typename F>
948 void parallel_for_each(R&& range, F&& func)
949 {
950 parallel_for_each(std::ranges::begin(range), std::ranges::end(range), std::forward<F>(func));
951 }
953#endif
954
957
959 [[nodiscard]] auto size() const noexcept -> size_t
960 {
961 return num_threads_;
962 }
963
965 [[nodiscard]] auto pending_tasks() const -> size_t
966 {
967 size_t total = 0;
968 for (auto const& queue : worker_queues_)
969 {
970 total += queue->size();
971 }
972
973 std::lock_guard<std::mutex> lock(overflow_mutex_);
974 total += overflow_tasks_.size();
975 return total;
976 }
977
980 {
981 auto const now = std::chrono::steady_clock::now();
982 auto const elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
983
984 Statistics stats;
985 stats.total_threads = num_threads_;
986 stats.active_threads = active_tasks_.load(std::memory_order_acquire);
988 stats.completed_tasks = completed_tasks_.load(std::memory_order_acquire);
989 stats.stolen_tasks = stolen_tasks_.load(std::memory_order_acquire);
990
991 if (elapsed.count() > 0)
992 {
993 stats.tasks_per_second = static_cast<double>(stats.completed_tasks) / elapsed.count();
994 }
995 else
996 {
997 stats.tasks_per_second = 0.0;
998 }
999
1000 auto const total_task_time = total_task_time_.load(std::memory_order_acquire);
1001 if (stats.completed_tasks > 0)
1002 {
1003 stats.avg_task_time = std::chrono::microseconds(total_task_time / stats.completed_tasks);
1004 }
1005 else
1006 {
1007 stats.avg_task_time = std::chrono::microseconds(0);
1008 }
1009
1010 return stats;
1011 }
1012
1014
1017
1026 auto configure_threads(std::string const& name_prefix, SchedulingPolicy policy = SchedulingPolicy::OTHER,
1028 {
1029 return detail::configure_worker_threads(workers_, name_prefix, policy, priority);
1030 }
1031
1034 {
1035 return detail::set_worker_affinity(workers_, affinity);
1036 }
1037
1043
1045
1048
1051 {
1052 std::unique_lock<std::mutex> lock(completion_mutex_);
1053 completion_condition_.wait(lock, [this] { return outstanding_tasks_.load(std::memory_order_acquire) == 0; });
1054 }
1055
1057
1060
1066 {
1067 std::lock_guard<std::mutex> lock(trace_mutex_);
1068 on_task_start_ = TaskStartCallbackStorage(std::move(cb));
1069 }
1070
1071 template <typename Callback,
1072 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, TaskStartCallback>, int> = 0>
1073 void set_on_task_start(Callback&& cb)
1074 {
1075 static_assert(std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id>,
1076 "Task start callback must accept (time_point, std::thread::id)");
1077 std::lock_guard<std::mutex> lock(trace_mutex_);
1079 std::forward<Callback>(cb));
1080 }
1081
1088 {
1089 std::lock_guard<std::mutex> lock(trace_mutex_);
1090 on_task_end_ = TaskEndCallbackStorage(std::move(cb));
1091 }
1092
1093 template <typename Callback,
1094 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, TaskEndCallback>, int> = 0>
1095 void set_on_task_end(Callback&& cb)
1096 {
1097 static_assert(
1098 std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id,
1099 std::chrono::microseconds>,
1100 "Task end callback must accept (time_point, std::thread::id, std::chrono::microseconds)");
1101 std::lock_guard<std::mutex> lock(trace_mutex_);
1102 on_task_end_ =
1103 detail::make_copyable_callable<void(std::chrono::steady_clock::time_point, std::thread::id,
1104 std::chrono::microseconds)>(std::forward<Callback>(cb));
1105 }
1106
1108
1109 private:
1110 size_t num_threads_;
1111 bool register_workers_;
1112 std::vector<ThreadWrapper> workers_;
1113 std::vector<std::unique_ptr<WorkStealingDeque<QueuedTask>>> worker_queues_;
1114
1115 std::queue<QueuedTask> overflow_tasks_;
1116 mutable std::mutex overflow_mutex_;
1117
1118 std::atomic<bool> stop_;
1119 std::condition_variable wakeup_condition_;
1120 std::mutex wakeup_mutex_;
1121
1122 std::condition_variable completion_condition_;
1123 std::mutex completion_mutex_;
1124
1125 std::atomic<size_t> next_victim_;
1126 std::atomic<size_t> active_tasks_{0};
1127 std::atomic<size_t> outstanding_tasks_{0};
1128 std::atomic<size_t> completed_tasks_{0};
1129 std::atomic<size_t> stolen_tasks_{0};
1130 std::atomic<uint64_t> total_task_time_{0};
1131
1132 std::mutex trace_mutex_;
1133 TaskStartCallbackStorage on_task_start_;
1134 TaskEndCallbackStorage on_task_end_;
1135
1136 std::chrono::steady_clock::time_point start_time_;
1137
1138 // NOLINTNEXTLINE(readability-function-cognitive-complexity)
1139 void worker_function(size_t worker_id)
1140 {
1141 std::optional<AutoRegisterCurrentThread> reg_guard;
1142 if (register_workers_)
1143 reg_guard.emplace("hp_worker_" + std::to_string(worker_id), "threadschedule.pool");
1144
1145 thread_local std::mt19937 gen = []() {
1146 std::random_device device;
1147 return std::mt19937(device());
1148 }();
1149
1150 QueuedTask task;
1151 std::uniform_int_distribution<size_t> dist(0, num_threads_ - 1);
1152
1153 while (true)
1154 {
1155 bool found_task = false;
1156
1157 if (worker_queues_[worker_id]->pop(task))
1158 {
1159 found_task = true;
1160 }
1161 else
1162 {
1163 size_t const max_steal_attempts = (std::min)(num_threads_, size_t(4));
1164 for (size_t attempts = 0; attempts < max_steal_attempts; ++attempts)
1165 {
1166 size_t const victim_id = dist(gen);
1167 if (victim_id != worker_id && worker_queues_[victim_id]->steal(task))
1168 {
1169 found_task = true;
1170 stolen_tasks_.fetch_add(1, std::memory_order_relaxed);
1171 break;
1172 }
1173 }
1174 }
1175
1176 if (!found_task)
1177 {
1178 std::lock_guard<std::mutex> lock(overflow_mutex_);
1179 if (!overflow_tasks_.empty())
1180 {
1181 task = std::move(overflow_tasks_.front());
1182 overflow_tasks_.pop();
1183 found_task = true;
1184 }
1185 }
1186
1187 if (found_task)
1188 {
1189 active_tasks_.fetch_add(1, std::memory_order_relaxed);
1190
1191 auto const start_time = std::chrono::steady_clock::now();
1192 auto const tid = std::this_thread::get_id();
1193
1194 TaskStartCallbackStorage on_task_start;
1195 {
1196 std::lock_guard<std::mutex> tl(trace_mutex_);
1197 on_task_start = on_task_start_;
1198 }
1199 if (on_task_start)
1200 on_task_start(start_time, tid);
1201
1202 // For submit() tasks the callable is a packaged_task which
1203 // catches exceptions internally and stores them in the
1204 // std::future shared state - those never reach this catch.
1205 // For post() tasks (fire-and-forget) the catch prevents an
1206 // unhandled exception from terminating the worker thread.
1207 try
1208 {
1209 task();
1210 }
1211 catch (...)
1212 {
1213 }
1214 auto const end_time = std::chrono::steady_clock::now();
1215
1216 auto const task_duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
1217 total_task_time_.fetch_add(task_duration.count(), std::memory_order_relaxed);
1218
1219 TaskEndCallbackStorage on_task_end;
1220 {
1221 std::lock_guard<std::mutex> tl(trace_mutex_);
1222 on_task_end = on_task_end_;
1223 }
1224 if (on_task_end)
1225 on_task_end(end_time, tid, task_duration);
1226
1227 active_tasks_.fetch_sub(1, std::memory_order_relaxed);
1228 {
1229 std::lock_guard<std::mutex> lock(completion_mutex_);
1230 outstanding_tasks_.fetch_sub(1, std::memory_order_acq_rel);
1231 }
1232 completed_tasks_.fetch_add(1, std::memory_order_relaxed);
1233
1234 completion_condition_.notify_all();
1235 wakeup_condition_.notify_all();
1236 }
1237 else
1238 {
1239 if (stop_.load(std::memory_order_acquire)
1240 && outstanding_tasks_.load(std::memory_order_acquire) == 0)
1241 {
1242 break;
1243 }
1244
1245 std::unique_lock<std::mutex> lock(wakeup_mutex_);
1246 wakeup_condition_.wait_for(lock, std::chrono::microseconds(100));
1247 }
1248 }
1249 }
1250};
1251
1252// ---------------------------------------------------------------------------
1253// Wait policies for ThreadPoolBase
1254// ---------------------------------------------------------------------------
1255
1263{
1264 template <typename Lock, typename Pred>
1265 static auto wait(std::condition_variable& cv, Lock& lock, Pred pred) -> bool
1266 {
1267 cv.wait(lock, pred);
1268 return true;
1269 }
1270};
1271
1281template <unsigned IntervalMs = 10>
1283{
1284 template <typename Lock, typename Pred>
1285 static auto wait(std::condition_variable& cv, Lock& lock, Pred pred) -> bool
1286 {
1287 return cv.wait_for(lock, std::chrono::milliseconds(IntervalMs), pred);
1288 }
1289};
1290
1291// ---------------------------------------------------------------------------
1292// ThreadPoolBase
1293// ---------------------------------------------------------------------------
1294
1347template <typename WaitPolicy>
1349{
1350 public:
1351 using Task = std::function<void()>;
1353
1355 {
1361 std::chrono::microseconds avg_task_time;
1362 };
1363
1364 explicit ThreadPoolBase(size_t num_threads = std::thread::hardware_concurrency(), bool register_workers = false)
1365 : num_threads_(num_threads == 0 ? 1 : num_threads), register_workers_(register_workers), stop_(false),
1366 start_time_(std::chrono::steady_clock::now())
1367 {
1368 workers_.reserve(num_threads_);
1369
1370 for (size_t i = 0; i < num_threads_; ++i)
1371 {
1372 workers_.emplace_back(&ThreadPoolBase::worker_function, this, i);
1373 }
1374 }
1375
1377 auto operator=(ThreadPoolBase const&) -> ThreadPoolBase& = delete;
1378
1383
1386
1392 template <typename F, typename... Args>
1393 auto try_submit(F&& f, Args&&... args) -> expected<std::future<std::invoke_result_t<F, Args...>>, std::error_code>
1394 {
1395 using return_type = std::invoke_result_t<F, Args...>;
1396
1397 auto task = std::make_shared<std::packaged_task<return_type()>>(
1398 detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
1399
1400 std::future<return_type> result = task->get_future();
1401
1402 {
1403 std::lock_guard<std::mutex> lock(queue_mutex_);
1404 if (stop_)
1405 return unexpected(std::make_error_code(std::errc::operation_canceled));
1406 tasks_.emplace([task]() { (*task)(); });
1407 }
1408
1409 condition_.notify_one();
1410 return result;
1411 }
1412
1417 template <typename F, typename... Args>
1418 auto submit(F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>
1419 {
1420 auto result = try_submit(std::forward<F>(f), std::forward<Args>(args)...);
1421 if (!result.has_value())
1422 throw std::runtime_error("Pool is shutting down");
1423 return std::move(result.value());
1424 }
1425
1434 template <typename F, typename... Args>
1435 void post(F&& f, Args&&... args)
1436 {
1437 auto r = try_post(std::forward<F>(f), std::forward<Args>(args)...);
1438 if (!r.has_value())
1439 throw std::runtime_error("Pool is shutting down");
1440 }
1441
1447 template <typename F, typename... Args>
1448 auto try_post(F&& f, Args&&... args) -> expected<void, std::error_code>
1449 {
1450 {
1451 std::lock_guard<std::mutex> lock(queue_mutex_);
1452 if (stop_)
1453 return unexpected(std::make_error_code(std::errc::operation_canceled));
1454 tasks_.emplace(detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
1455 }
1456 condition_.notify_one();
1457 return {};
1458 }
1459
1460#if __cpp_lib_jthread >= 201911L
1467 template <typename F, typename... Args>
1468 auto submit(std::stop_token token, F&& f, Args&&... args) -> std::future<std::invoke_result_t<F, Args...>>
1469 {
1470 return submit([token = std::move(token),
1471 bound = detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
1472 if (token.stop_requested())
1473 return std::invoke_result_t<F, Args...>();
1474 return bound();
1475 });
1476 }
1477
1479 template <typename F, typename... Args>
1480 auto try_submit(std::stop_token token, F&& f, Args&&... args)
1481 -> expected<std::future<std::invoke_result_t<F, Args...>>, std::error_code>
1482 {
1483 return try_submit([token = std::move(token),
1484 bound = detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...)]() mutable {
1485 if (token.stop_requested())
1486 return std::invoke_result_t<F, Args...>();
1487 return bound();
1488 });
1489 }
1490#endif
1491
1497 template <typename Iterator>
1498 auto try_submit_batch(Iterator begin, Iterator end) -> expected<std::vector<std::future<void>>, std::error_code>
1499 {
1500 std::vector<std::future<void>> futures;
1501 futures.reserve(std::distance(begin, end));
1502
1503 {
1504 std::lock_guard<std::mutex> lock(queue_mutex_);
1505 if (stop_)
1506 return unexpected(std::make_error_code(std::errc::operation_canceled));
1507
1508 for (auto it = begin; it != end; ++it)
1509 {
1510 auto task = std::make_shared<std::packaged_task<void()>>(*it);
1511 futures.push_back(task->get_future());
1512 tasks_.emplace([task]() { (*task)(); });
1513 }
1514 }
1515
1516 condition_.notify_all();
1517 return futures;
1518 }
1519
1521 template <typename Iterator>
1522 auto submit_batch(Iterator begin, Iterator end) -> std::vector<std::future<void>>
1523 {
1524 auto result = try_submit_batch(begin, end);
1525 if (!result.has_value())
1526 throw std::runtime_error("Pool is shutting down");
1527 return std::move(result.value());
1528 }
1529
1531 template <typename Iterator, typename F>
1532 void parallel_for_each(Iterator begin, Iterator end, F&& func)
1533 {
1534 detail::parallel_for_each_chunked(*this, begin, end, std::forward<F>(func), num_threads_);
1535 }
1536
1537#if __cpp_lib_ranges >= 201911L
1539 template <std::ranges::input_range R>
1540 auto submit_batch(R&& range)
1541 {
1542 return submit_batch(std::ranges::begin(range), std::ranges::end(range));
1543 }
1544
1545 template <std::ranges::input_range R>
1546 auto try_submit_batch(R&& range)
1547 {
1548 return try_submit_batch(std::ranges::begin(range), std::ranges::end(range));
1549 }
1550
1551 template <std::ranges::input_range R, typename F>
1552 void parallel_for_each(R&& range, F&& func)
1553 {
1554 parallel_for_each(std::ranges::begin(range), std::ranges::end(range), std::forward<F>(func));
1555 }
1557#endif
1558
1560
1563
1565 [[nodiscard]] auto size() const noexcept -> size_t
1566 {
1567 return num_threads_;
1568 }
1569
1571 [[nodiscard]] auto pending_tasks() const -> size_t
1572 {
1573 std::lock_guard<std::mutex> lock(queue_mutex_);
1574 return tasks_.size();
1575 }
1576
1578
1581
1586 auto configure_threads(std::string const& name_prefix, SchedulingPolicy policy = SchedulingPolicy::OTHER,
1588 {
1589 return detail::configure_worker_threads(workers_, name_prefix, policy, priority);
1590 }
1591
1594 {
1595 return detail::set_worker_affinity(workers_, affinity);
1596 }
1597
1603
1605
1608
1611 {
1612 std::unique_lock<std::mutex> lock(queue_mutex_);
1613 task_finished_condition_.wait(
1614 lock, [this] { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
1615 }
1616
1623 {
1624 {
1625 std::lock_guard<std::mutex> lock(queue_mutex_);
1626 if (stop_)
1627 return;
1628 stop_ = true;
1629 if (policy == ShutdownPolicy::drop_pending)
1630 {
1631 std::queue<QueuedTask> empty;
1632 tasks_.swap(empty);
1633 }
1634 }
1635
1636 condition_.notify_all();
1637
1638 for (auto& worker : workers_)
1639 {
1640 if (worker.joinable())
1641 worker.join();
1642 }
1643
1644 workers_.clear();
1645 }
1646
1653 auto shutdown_for(std::chrono::milliseconds timeout) -> bool
1654 {
1655 auto const deadline = std::chrono::steady_clock::now() + timeout;
1656
1657 {
1658 std::lock_guard<std::mutex> lock(queue_mutex_);
1659 if (stop_)
1660 return true;
1661 }
1662
1663 std::unique_lock<std::mutex> lock(queue_mutex_);
1664 bool const drained = task_finished_condition_.wait_until(
1665 lock, deadline, [this] { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
1666 lock.unlock();
1667
1669 return drained;
1670 }
1671
1673
1676
1678 [[nodiscard]] auto get_statistics() const -> Statistics
1679 {
1680 auto const now = std::chrono::steady_clock::now();
1681 auto const elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
1682
1683 std::lock_guard<std::mutex> lock(queue_mutex_);
1684 Statistics stats;
1685 stats.total_threads = num_threads_;
1686 stats.active_threads = active_tasks_.load(std::memory_order_acquire);
1687 stats.pending_tasks = tasks_.size();
1688 stats.completed_tasks = completed_tasks_.load(std::memory_order_acquire);
1689
1690 if (elapsed.count() > 0)
1691 {
1692 stats.tasks_per_second = static_cast<double>(stats.completed_tasks) / elapsed.count();
1693 }
1694 else
1695 {
1696 stats.tasks_per_second = 0.0;
1697 }
1698
1699 auto const total_task_time = total_task_time_.load(std::memory_order_acquire);
1700 if (stats.completed_tasks > 0)
1701 {
1702 stats.avg_task_time = std::chrono::microseconds(total_task_time / stats.completed_tasks);
1703 }
1704 else
1705 {
1706 stats.avg_task_time = std::chrono::microseconds(0);
1707 }
1708
1709 return stats;
1710 }
1711
1713
1716
1722 {
1723 std::lock_guard<std::mutex> lock(trace_mutex_);
1724 on_task_start_ = TaskStartCallbackStorage(std::move(cb));
1725 }
1726
1727 template <typename Callback,
1728 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, TaskStartCallback>, int> = 0>
1729 void set_on_task_start(Callback&& cb)
1730 {
1731 static_assert(std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id>,
1732 "Task start callback must accept (time_point, std::thread::id)");
1733 std::lock_guard<std::mutex> lock(trace_mutex_);
1735 std::forward<Callback>(cb));
1736 }
1737
1744 {
1745 std::lock_guard<std::mutex> lock(trace_mutex_);
1746 on_task_end_ = TaskEndCallbackStorage(std::move(cb));
1747 }
1748
1749 template <typename Callback,
1750 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, TaskEndCallback>, int> = 0>
1751 void set_on_task_end(Callback&& cb)
1752 {
1753 static_assert(
1754 std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id,
1755 std::chrono::microseconds>,
1756 "Task end callback must accept (time_point, std::thread::id, std::chrono::microseconds)");
1757 std::lock_guard<std::mutex> lock(trace_mutex_);
1758 on_task_end_ =
1759 detail::make_copyable_callable<void(std::chrono::steady_clock::time_point, std::thread::id,
1760 std::chrono::microseconds)>(std::forward<Callback>(cb));
1761 }
1762
1764
1765 private:
1766 size_t num_threads_;
1767 bool register_workers_;
1768 std::vector<ThreadWrapper> workers_;
1769 std::queue<QueuedTask> tasks_;
1770
1771 mutable std::mutex queue_mutex_;
1772 std::condition_variable condition_;
1773 std::condition_variable task_finished_condition_;
1774 std::atomic<bool> stop_;
1775 std::atomic<size_t> active_tasks_{0};
1776 std::atomic<size_t> completed_tasks_{0};
1777 std::atomic<uint64_t> total_task_time_{0};
1778
1779 std::mutex trace_mutex_;
1780 TaskStartCallbackStorage on_task_start_;
1781 TaskEndCallbackStorage on_task_end_;
1782
1783 std::chrono::steady_clock::time_point start_time_;
1784
1785 void worker_function(size_t worker_id)
1786 {
1787 std::optional<AutoRegisterCurrentThread> reg_guard;
1788 if (register_workers_)
1789 reg_guard.emplace("pool_worker_" + std::to_string(worker_id), "threadschedule.pool");
1790
1791 while (true)
1792 {
1793 QueuedTask task;
1794 bool found_task = false;
1795
1796 {
1797 std::unique_lock<std::mutex> lock(queue_mutex_);
1798
1799 if (WaitPolicy::wait(condition_, lock, [this] { return stop_ || !tasks_.empty(); }))
1800 {
1801 if (stop_ && tasks_.empty())
1802 {
1803 return;
1804 }
1805
1806 if (!tasks_.empty())
1807 {
1808 task = std::move(tasks_.front());
1809 tasks_.pop();
1810 found_task = true;
1811 active_tasks_.fetch_add(1, std::memory_order_relaxed);
1812 }
1813 }
1814 else if (stop_)
1815 {
1816 return;
1817 }
1818 }
1819
1820 if (found_task)
1821 {
1822 auto const start_time = std::chrono::steady_clock::now();
1823 auto const tid = std::this_thread::get_id();
1824
1825 TaskStartCallbackStorage on_task_start;
1826 {
1827 std::lock_guard<std::mutex> tl(trace_mutex_);
1828 on_task_start = on_task_start_;
1829 }
1830 if (on_task_start)
1831 on_task_start(start_time, tid);
1832
1833 // See HighPerformancePool::worker_function for rationale.
1834 try
1835 {
1836 task();
1837 }
1838 catch (...)
1839 {
1840 }
1841 auto const end_time = std::chrono::steady_clock::now();
1842
1843 auto const task_duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
1844 total_task_time_.fetch_add(task_duration.count(), std::memory_order_relaxed);
1845
1846 TaskEndCallbackStorage on_task_end;
1847 {
1848 std::lock_guard<std::mutex> tl(trace_mutex_);
1849 on_task_end = on_task_end_;
1850 }
1851 if (on_task_end)
1852 on_task_end(end_time, tid, task_duration);
1853
1854 {
1855 std::lock_guard<std::mutex> lock(queue_mutex_);
1856 active_tasks_.fetch_sub(1, std::memory_order_relaxed);
1857 }
1858 completed_tasks_.fetch_add(1, std::memory_order_relaxed);
1859
1860 task_finished_condition_.notify_all();
1861 }
1862 }
1863 }
1864};
1865
1877
1888
1889// ---------------------------------------------------------------------------
1890// LightweightPoolT
1891// ---------------------------------------------------------------------------
1892
1961template <size_t TaskSize = 64>
1963{
1964 public:
1970 explicit LightweightPoolT(size_t num_threads = std::thread::hardware_concurrency())
1971 : num_threads_(num_threads == 0 ? 1 : num_threads)
1972 {
1973 workers_.reserve(num_threads_);
1974 for (size_t i = 0; i < num_threads_; ++i)
1975 workers_.emplace_back(&LightweightPoolT::worker_loop, this);
1976 }
1977
1980
1985
1988
2000 template <typename F, typename... Args>
2001 void post(F&& f, Args&&... args)
2002 {
2003 auto r = try_post(std::forward<F>(f), std::forward<Args>(args)...);
2004 if (!r.has_value())
2005 throw std::runtime_error("LightweightPool is shutting down");
2006 }
2007
2014 template <typename F, typename... Args>
2015 auto try_post(F&& f, Args&&... args) -> expected<void, std::error_code>
2016 {
2017 detail::SboCallable<TaskSize> task(detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
2018 {
2019 std::lock_guard<std::mutex> lock(mutex_);
2020 if (stop_)
2021 return unexpected(std::make_error_code(std::errc::operation_canceled));
2022 tasks_.push(std::move(task));
2023 }
2024 condition_.notify_one();
2025 return {};
2026 }
2027
2037 template <typename Iterator>
2038 void post_batch(Iterator begin, Iterator end)
2039 {
2040 auto r = try_post_batch(begin, end);
2041 if (!r.has_value())
2042 throw std::runtime_error("LightweightPool is shutting down");
2043 }
2044
2049 template <typename Iterator>
2050 auto try_post_batch(Iterator begin, Iterator end) -> expected<void, std::error_code>
2051 {
2052 {
2053 std::lock_guard<std::mutex> lock(mutex_);
2054 if (stop_)
2055 return unexpected(std::make_error_code(std::errc::operation_canceled));
2056 for (auto it = begin; it != end; ++it)
2057 tasks_.push(detail::SboCallable<TaskSize>(*it));
2058 }
2059 condition_.notify_all();
2060 return {};
2061 }
2062
2063#if __cpp_lib_ranges >= 201911L
2065 template <std::ranges::input_range R>
2066 void post_batch(R&& range)
2067 {
2068 post_batch(std::ranges::begin(range), std::ranges::end(range));
2069 }
2070
2071 template <std::ranges::input_range R>
2072 auto try_post_batch(R&& range)
2073 {
2074 return try_post_batch(std::ranges::begin(range), std::ranges::end(range));
2075 }
2077#endif
2078
2080
2083
2095 {
2096 {
2097 std::lock_guard<std::mutex> lock(mutex_);
2098 if (stop_)
2099 return;
2100 stop_ = true;
2101 if (policy == ShutdownPolicy::drop_pending)
2102 {
2103 std::queue<detail::SboCallable<TaskSize>> empty;
2104 tasks_.swap(empty);
2105 }
2106 }
2107 condition_.notify_all();
2108 for (auto& w : workers_)
2109 {
2110 if (w.joinable())
2111 w.join();
2112 }
2113 workers_.clear();
2114 }
2115
2125 auto shutdown_for(std::chrono::milliseconds timeout) -> bool
2126 {
2127 auto const deadline = std::chrono::steady_clock::now() + timeout;
2128 {
2129 std::lock_guard<std::mutex> lock(mutex_);
2130 if (stop_)
2131 return true;
2132 }
2133 std::unique_lock<std::mutex> lock(mutex_);
2134 bool const drained = drain_condition_.wait_until(
2135 lock, deadline, [this] { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
2136 lock.unlock();
2138 return drained;
2139 }
2140
2142
2145
2147 [[nodiscard]] auto size() const noexcept -> size_t
2148 {
2149 return num_threads_;
2150 }
2151
2153
2156
2162 auto configure_threads(std::string const& name_prefix, SchedulingPolicy policy = SchedulingPolicy::OTHER,
2164 {
2165 return detail::configure_worker_threads(workers_, name_prefix, policy, priority);
2166 }
2167
2170 {
2171 return detail::set_worker_affinity(workers_, affinity);
2172 }
2173
2179
2181
2182 private:
2183 size_t num_threads_;
2184 std::vector<ThreadWrapper> workers_;
2185 std::queue<detail::SboCallable<TaskSize>> tasks_;
2186 std::mutex mutex_;
2187 std::condition_variable condition_;
2188 std::condition_variable drain_condition_;
2189 std::atomic<bool> stop_{false};
2190 std::atomic<size_t> active_tasks_{0};
2191
2192 void worker_loop()
2193 {
2194 while (true)
2195 {
2196 detail::SboCallable<TaskSize> task;
2197 {
2198 std::unique_lock<std::mutex> lock(mutex_);
2199 condition_.wait(lock, [this] { return stop_ || !tasks_.empty(); });
2200 if (stop_ && tasks_.empty())
2201 return;
2202 if (!tasks_.empty())
2203 {
2204 task = std::move(tasks_.front());
2205 tasks_.pop();
2206 active_tasks_.fetch_add(1, std::memory_order_relaxed);
2207 }
2208 else
2209 continue;
2210 }
2211 try
2212 {
2213 task();
2214 }
2215 catch (...)
2216 {
2217 }
2218 active_tasks_.fetch_sub(1, std::memory_order_relaxed);
2219 drain_condition_.notify_all();
2220 }
2221 }
2222};
2223
2233
2234// ---------------------------------------------------------------------------
2235// GlobalPool
2236// ---------------------------------------------------------------------------
2237
2265template <typename PoolType>
2266class GlobalPool
2267{
2268 public:
2275 static void init(size_t num_threads)
2276 {
2277 std::call_once(init_flag_(), [num_threads] { thread_count_() = num_threads; });
2278 }
2279
2281 static auto instance() -> PoolType&
2282 {
2283 static PoolType pool(thread_count_());
2284 return pool;
2285 }
2286
2290
2291 template <typename F, typename... Args>
2292 static auto submit(F&& f, Args&&... args)
2293 {
2294 return instance().submit(std::forward<F>(f), std::forward<Args>(args)...);
2295 }
2296
2297 template <typename F, typename... Args>
2298 static auto try_submit(F&& f, Args&&... args)
2299 {
2300 return instance().try_submit(std::forward<F>(f), std::forward<Args>(args)...);
2301 }
2302
2303 template <typename F, typename... Args>
2304 static void post(F&& f, Args&&... args)
2305 {
2306 instance().post(std::forward<F>(f), std::forward<Args>(args)...);
2307 }
2308
2309 template <typename F, typename... Args>
2310 static auto try_post(F&& f, Args&&... args)
2311 {
2312 return instance().try_post(std::forward<F>(f), std::forward<Args>(args)...);
2313 }
2314
2315 template <typename Iterator>
2316 static auto submit_batch(Iterator begin, Iterator end)
2317 {
2318 return instance().submit_batch(begin, end);
2319 }
2320
2321 template <typename Iterator>
2322 static auto try_submit_batch(Iterator begin, Iterator end)
2323 {
2324 return instance().try_submit_batch(begin, end);
2325 }
2326
2327 template <typename Iterator, typename F>
2328 static void parallel_for_each(Iterator begin, Iterator end, F&& func)
2329 {
2330 instance().parallel_for_each(begin, end, std::forward<F>(func));
2331 }
2332
2333#if __cpp_lib_ranges >= 201911L
2334 template <std::ranges::input_range R>
2335 static auto submit_batch(R&& range)
2336 {
2337 return instance().submit_batch(std::forward<R>(range));
2338 }
2339
2340 template <std::ranges::input_range R>
2341 static auto try_submit_batch(R&& range)
2342 {
2343 return instance().try_submit_batch(std::forward<R>(range));
2344 }
2345
2346 template <std::ranges::input_range R, typename F>
2347 static void parallel_for_each(R&& range, F&& func)
2348 {
2349 instance().parallel_for_each(std::forward<R>(range), std::forward<F>(func));
2350 }
2351#endif
2352
2354
2355 private:
2356 GlobalPool() = default;
2357
2358 static auto init_flag_() -> std::once_flag&
2359 {
2360 static std::once_flag flag;
2361 return flag;
2362 }
2363
2364 static auto thread_count_() -> size_t&
2365 {
2366 static size_t count = std::thread::hardware_concurrency();
2367 return count;
2368 }
2369};
2370
2376
2382
2400template <typename Container, typename F>
2401void parallel_for_each(Container& container, F&& func)
2402{
2403 GlobalThreadPool::parallel_for_each(container.begin(), container.end(), std::forward<F>(func));
2404}
2405
2406} // namespace threadschedule
Feature-gated callable storage aliases for modern C++ builds.
Singleton accessor for a process-wide pool instance.
static auto submit_batch(Iterator begin, Iterator end)
static auto try_submit(F &&f, Args &&... args)
static auto try_post(F &&f, Args &&... args)
static void parallel_for_each(Iterator begin, Iterator end, F &&func)
static auto submit(F &&f, Args &&... args)
static void post(F &&f, Args &&... args)
static auto instance() -> PoolType &
Access the singleton pool instance (created on first call).
static auto try_submit_batch(Iterator begin, Iterator end)
static void init(size_t num_threads)
Pre-configure the number of threads before first use.
void post(F &&f, Args &&... args)
Fire-and-forget task submission (throwing variant).
auto operator=(HighPerformancePool const &) -> HighPerformancePool &=delete
auto submit(F &&f, Args &&... args) -> std::future< std::invoke_result_t< F, Args... > >
Submit a task, throwing on shutdown.
void set_on_task_end(TaskEndCallback cb)
Register a callback invoked just after each task completes.
auto size() const noexcept -> size_t
Number of worker threads in this pool.
void shutdown(ShutdownPolicy policy=ShutdownPolicy::drain)
Shut the pool down.
HighPerformancePool(size_t num_threads=std::thread::hardware_concurrency(), size_t deque_capacity=WorkStealingDeque< QueuedTask >::DEFAULT_CAPACITY, bool register_workers=false)
auto try_submit_batch(Iterator begin, Iterator end) -> expected< std::vector< std::future< void > >, std::error_code >
Submit a range of void() callables in one go (non-throwing).
auto try_submit(F &&f, Args &&... args) -> expected< std::future< std::invoke_result_t< F, Args... > >, std::error_code >
Submit a task without throwing on shutdown.
detail::move_callable< void()> QueuedTask
HighPerformancePool(HighPerformancePool const &)=delete
void set_on_task_start(TaskStartCallback cb)
Register a callback invoked just before each task executes.
auto try_post(F &&f, Args &&... args) -> expected< void, std::error_code >
Fire-and-forget task submission (non-throwing variant).
auto submit_batch(Iterator begin, Iterator end) -> std::vector< std::future< void > >
Submit a range of void() callables in one go (throwing).
void wait_for_tasks()
Block until all pending and active tasks have completed.
auto set_affinity(ThreadAffinity const &affinity) -> expected< void, std::error_code >
Pin all workers to the same CPU set.
auto distribute_across_cpus() -> expected< void, std::error_code >
Pin each worker to a distinct CPU core (round-robin).
auto pending_tasks() const -> size_t
Approximate count of tasks waiting in all queues.
void parallel_for_each(Iterator begin, Iterator end, F &&func)
Apply func to every element in [begin, end) in parallel.
auto shutdown_for(std::chrono::milliseconds timeout) -> bool
Attempt a timed drain: finish as many tasks as possible within timeout, then force-stop remaining wor...
auto get_statistics() const -> Statistics
Collect approximate performance counters.
auto configure_threads(std::string const &name_prefix, SchedulingPolicy policy=SchedulingPolicy::OTHER, ThreadPriority priority=ThreadPriority::normal()) -> expected< void, std::error_code >
Name, schedule and prioritize all worker threads.
Ultra-lightweight fire-and-forget thread pool.
auto try_post_batch(Iterator begin, Iterator end) -> expected< void, std::error_code >
Batch post (non-throwing).
auto operator=(LightweightPoolT const &) -> LightweightPoolT &=delete
void post_batch(Iterator begin, Iterator end)
Post a range of callables under a single lock acquisition.
auto shutdown_for(std::chrono::milliseconds timeout) -> bool
Attempt a timed drain.
void shutdown(ShutdownPolicy policy=ShutdownPolicy::drain)
Shut the pool down.
LightweightPoolT(size_t num_threads=std::thread::hardware_concurrency())
Construct a lightweight pool with num_threads workers.
auto try_post(F &&f, Args &&... args) -> expected< void, std::error_code >
Post a fire-and-forget task (non-throwing variant).
auto set_affinity(ThreadAffinity const &affinity) -> expected< void, std::error_code >
Pin all workers to the same CPU set.
LightweightPoolT(LightweightPoolT const &)=delete
void post(F &&f, Args &&... args)
Post a fire-and-forget task (throwing variant).
auto size() const noexcept -> size_t
Number of worker threads.
auto configure_threads(std::string const &name_prefix, SchedulingPolicy policy=SchedulingPolicy::OTHER, ThreadPriority priority=ThreadPriority::normal()) -> expected< void, std::error_code >
Name, schedule and prioritize all worker threads.
auto distribute_across_cpus() -> expected< void, std::error_code >
Pin each worker to a distinct CPU core (round-robin).
Manages a set of CPU indices to which a thread may be bound.
Single-queue thread pool parameterized by its idle-wait strategy.
void parallel_for_each(Iterator begin, Iterator end, F &&func)
Apply func to [begin, end) in parallel (chunked).
auto set_affinity(ThreadAffinity const &affinity) -> expected< void, std::error_code >
Pin all workers to the same CPU set.
void set_on_task_start(TaskStartCallback cb)
Register a callback invoked just before each task executes.
void set_on_task_start(Callback &&cb)
auto shutdown_for(std::chrono::milliseconds timeout) -> bool
Attempt a timed drain: finish as many tasks as possible within timeout, then force-stop remaining wor...
auto operator=(ThreadPoolBase const &) -> ThreadPoolBase &=delete
auto try_submit(F &&f, Args &&... args) -> expected< std::future< std::invoke_result_t< F, Args... > >, std::error_code >
Submit a task without throwing on shutdown.
void wait_for_tasks()
Block until all pending and active tasks have completed.
auto distribute_across_cpus() -> expected< void, std::error_code >
Pin each worker to a distinct CPU core (round-robin).
void shutdown(ShutdownPolicy policy=ShutdownPolicy::drain)
detail::move_callable< void()> QueuedTask
ThreadPoolBase(ThreadPoolBase const &)=delete
auto try_post(F &&f, Args &&... args) -> expected< void, std::error_code >
void post(F &&f, Args &&... args)
Fire-and-forget task submission (throwing variant).
auto configure_threads(std::string const &name_prefix, SchedulingPolicy policy=SchedulingPolicy::OTHER, ThreadPriority priority=ThreadPriority::normal()) -> expected< void, std::error_code >
Name, schedule and prioritize all worker threads.
std::function< void()> Task
auto submit_batch(Iterator begin, Iterator end) -> std::vector< std::future< void > >
Submit a batch of tasks (throwing).
void set_on_task_end(TaskEndCallback cb)
Register a callback invoked just after each task completes.
auto pending_tasks() const -> size_t
Number of tasks waiting in the queue.
auto size() const noexcept -> size_t
Number of worker threads.
auto submit(F &&f, Args &&... args) -> std::future< std::invoke_result_t< F, Args... > >
Submit a task, throwing on shutdown.
ThreadPoolBase(size_t num_threads=std::thread::hardware_concurrency(), bool register_workers=false)
auto get_statistics() const -> Statistics
Collect approximate performance counters.
void set_on_task_end(Callback &&cb)
auto try_submit_batch(Iterator begin, Iterator end) -> expected< std::vector< std::future< void > >, std::error_code >
Submit a range of void() callables in one go (non-throwing).
Value-semantic wrapper for a thread scheduling priority.
static constexpr auto normal() noexcept -> ThreadPriority
static constexpr size_t DEFAULT_CAPACITY
auto push(T const &item) -> bool
static constexpr size_t CACHE_LINE_SIZE
WorkStealingDeque(size_t capacity=DEFAULT_CAPACITY)
Type-erased, move-only callable with configurable inline storage.
auto operator=(SboCallable const &) -> SboCallable &=delete
SboCallable(SboCallable &&other) noexcept
auto operator=(SboCallable &&other) noexcept -> SboCallable &
SboCallable(SboCallable const &)=delete
A result type that holds either a value of type T or an error of type E.
Definition expected.hpp:215
Exception thrown by expected::value() when the object is in the error state.
Definition expected.hpp:162
Polyfill for std::expected (C++23) for pre-C++23 compilers.
std::function< Signature > copyable_callable
Definition callable.hpp:40
auto bind_args(F &&f, Args &&... args)
Bind a callable with its arguments into a nullary lambda.
auto distribute_workers_across_cpus(WorkerRange &workers) -> expected< void, std::error_code >
auto make_move_callable(Callable &&callable) -> move_callable< Signature >
Definition callable.hpp:111
std::function< Signature > move_callable
Definition callable.hpp:32
auto configure_worker_threads(WorkerRange &workers, std::string const &name_prefix, SchedulingPolicy policy, ThreadPriority priority) -> expected< void, std::error_code >
auto make_copyable_callable(Callable &&callable) -> copyable_callable< Signature >
Definition callable.hpp:117
auto set_worker_affinity(WorkerRange &workers, ThreadAffinity const &affinity) -> expected< void, std::error_code >
void parallel_for_each_chunked(Pool &pool, Iterator begin, Iterator end, F &&func, size_t num_workers)
SchedulingPolicy
Enumeration of available thread scheduling policies.
@ OTHER
Standard round-robin time-sharing.
ShutdownPolicy
Controls how a pool handles pending tasks during shutdown.
@ drop_pending
Finish running tasks, discard queued ones.
@ drain
Finish all queued tasks before stopping (default).
ThreadPoolBase< IndefiniteWait > ThreadPool
General-purpose thread pool with indefinite blocking wait.
void parallel_for_each(Container &container, F &&func)
Convenience wrapper that applies a callable to every element of a container in parallel using the Glo...
GlobalPool< ThreadPool > GlobalThreadPool
Singleton accessor for the process-wide ThreadPool instance.
TaskEndCallback TaskEndCallbackStorage
ThreadPoolBase< PollingWait<> > FastThreadPool
Thread pool with 10 ms polling wait for lower wake-up latency.
GlobalPool< HighPerformancePool > GlobalHighPerformancePool
Singleton accessor for the process-wide HighPerformancePool instance.
detail::copyable_callable< void(std::chrono::steady_clock::time_point, std::thread::id)> TaskStartCallback
Work-stealing deque for per-thread task queues in a thread pool.
detail::copyable_callable< void(std::chrono::steady_clock::time_point, std::thread::id, std::chrono::microseconds elapsed)> TaskEndCallback
Callback invoked when a pool worker finishes executing a task.
TaskStartCallback TaskStartCallbackStorage
LightweightPoolT<> LightweightPool
Default lightweight pool with 64-byte task slots (56 bytes usable).
Scheduling policies, thread priority, and CPU affinity types.
Wait policy that blocks indefinitely until work is available.
static auto wait(std::condition_variable &cv, Lock &lock, Pred pred) -> bool
Wait policy that polls with a configurable timeout.
static auto wait(std::condition_variable &cv, Lock &lock, Pred pred) -> bool
Process-wide thread registry, control blocks, and composite registry.
Enhanced thread wrappers: ThreadWrapper, JThreadWrapper, and non-owning views.