ThreadSchedule 3.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
thread_pool_backend_base.hpp
Go to the documentation of this file.
1#pragma once
2
10#include "../callable/bind.hpp"
11#include "../callable/move_only_function.hpp"
12#include "callbacks.hpp"
13#include "deadline.hpp"
14#include "indefinite_wait.hpp"
15#include "polling_wait.hpp"
18#include "worker_count.hpp"
19
20#include <atomic>
21#include <chrono>
22#include <condition_variable>
23#include <cstddef>
24#include <cstdint>
25#include <functional>
26#include <future>
27#include <memory>
28#include <mutex>
29#include <optional>
30#include <queue>
31#include <string>
32#include <system_error>
33#include <type_traits>
34#include <utility>
35#include <vector>
36
38{
39
40// ---------------------------------------------------------------------------
41// thread_pool_backend_base
42// ---------------------------------------------------------------------------
43
96template <typename WaitPolicy>
98{
99public:
100 using task_type = std::function<void()>;
102
104 {
110 std::chrono::microseconds avg_task_time;
111 };
112
113 explicit thread_pool_backend_base(size_t num_threads = default_worker_count(), bool register_workers = false)
114 : num_threads_(checked_worker_count(num_threads)), register_workers_(register_workers), stop_(false),
115 start_time_(std::chrono::steady_clock::now())
116 {
117 workers_.reserve(num_threads_);
118
119 try
120 {
121 for (size_t i = 0; i < num_threads_; ++i)
122 workers_.emplace_back(&thread_pool_backend_base::worker_function, this, i);
123 startup_.wait(num_threads_);
124 }
125 catch (...)
126 {
127 stop_.store(true, std::memory_order_release);
128 condition_.notify_all();
129 for (auto& worker : workers_)
130 if (worker.joinable())
131 worker.join();
132 throw;
133 }
134 }
135
138
143
146
152 template <typename F, typename... Args>
153 auto
154 try_submit(F&& f, Args&&... args) -> expected<std::future<bind_result_t<F, Args...>>, std::error_code>
155 {
156 using return_type = bind_result_t<F, Args...>;
157
158 auto task = std::make_shared<std::packaged_task<return_type()>>(
159 detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
160
161 std::future<return_type> result = task->get_future();
162
163 {
164 std::lock_guard<std::mutex> lock(queue_mutex_);
165 if (stop_)
166 return unexpected(std::make_error_code(std::errc::operation_canceled));
167 tasks_.emplace([task]() { (*task)(); });
168 }
169
170 condition_.notify_one();
171 return result;
172 }
173
178 template <typename F, typename... Args>
179 auto
180 submit(F&& f, Args&&... args) -> std::future<bind_result_t<F, Args...>>
181 {
182 auto result = try_submit(std::forward<F>(f), std::forward<Args>(args)...);
183 if (!result.has_value())
184 throw std::runtime_error("Pool is shutting down");
185 return std::move(result.value());
186 }
187
196 template <typename F, typename... Args>
197 void
198 post(F&& f, Args&&... args)
199 {
200 auto r = try_post(std::forward<F>(f), std::forward<Args>(args)...);
201 if (!r.has_value())
202 throw std::runtime_error("Pool is shutting down");
203 }
204
210 template <typename F, typename... Args>
211 auto
213 {
214 queued_task task(detail::bind_args(std::forward<F>(f), std::forward<Args>(args)...));
215 {
216 std::lock_guard<std::mutex> lock(queue_mutex_);
217 if (stop_)
218 return unexpected(std::make_error_code(std::errc::operation_canceled));
219 tasks_.push(std::move(task));
220 }
221 condition_.notify_one();
222 return {};
223 }
224
231 template <typename Iterator>
232 auto
233 try_submit_batch(Iterator begin, Iterator end) -> expected<std::vector<std::future<void>>, std::error_code>
234 {
235 std::vector<std::future<void>> futures;
236 size_t const batch_size_hint = detail::multipass_range_size(begin, end);
237 futures.reserve(batch_size_hint);
238 std::vector<std::shared_ptr<std::packaged_task<void()>>> prepared;
239 prepared.reserve(batch_size_hint);
240
241 for (auto it = begin; it != end; ++it)
242 {
243 auto task = std::make_shared<std::packaged_task<void()>>(*it);
244 futures.push_back(task->get_future());
245 prepared.push_back(std::move(task));
246 }
247
248 bool enqueued = false;
249 try
250 {
251 std::lock_guard<std::mutex> lock(queue_mutex_);
252 if (stop_)
253 return unexpected(std::make_error_code(std::errc::operation_canceled));
254
255 for (auto const& task : prepared)
256 {
257 tasks_.emplace([task]() { (*task)(); });
258 enqueued = true;
259 }
260 }
261 catch (...)
262 {
263 if (enqueued)
264 condition_.notify_all();
265 throw;
266 }
267
268 condition_.notify_all();
269 return futures;
270 }
271
273 template <typename Iterator>
274 auto
275 submit_batch(Iterator begin, Iterator end) -> std::vector<std::future<void>>
276 {
277 auto result = try_submit_batch(begin, end);
278 if (!result.has_value())
279 throw std::runtime_error("Pool is shutting down");
280 return std::move(result.value());
281 }
282
285 template <typename Iterator, typename F>
286 void
287 parallel_for_each(Iterator begin, Iterator end, F&& func)
288 {
289 if (is_current_worker())
291 detail::parallel_for_each_chunked(*this, begin, end, std::forward<F>(func), num_threads_);
292 }
293
295
298
300 [[nodiscard]] auto
301 size() const noexcept -> size_t
302 {
303 return num_threads_;
304 }
305
307 [[nodiscard]] auto
308 pending_tasks() const -> size_t
309 {
310 std::lock_guard<std::mutex> lock(queue_mutex_);
311 return tasks_.size();
312 }
313
315
318
323 auto
327 {
328 return detail::configure_worker_threads(workers_, name_prefix, policy, priority,
329 register_workers_ ? &runtime_registry() : nullptr);
330 }
331
332 auto
334 {
335 return detail::configure_worker_threads(workers_, config, register_workers_ ? &runtime_registry() : nullptr);
336 }
337
339 auto
341 {
342 return detail::set_worker_affinity(workers_, affinity);
343 }
344
346 auto
351
353
356
358 void
360 {
361 if (is_current_worker())
363 std::unique_lock<std::mutex> lock(queue_mutex_);
364 task_finished_condition_.wait(lock, [this]
365 { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
366 }
367
368 [[nodiscard]] auto
369 is_current_worker() const noexcept -> bool
370 {
371 return current_pool == this;
372 }
373
379 void
381 {
382 if (is_current_worker())
384 std::lock_guard<std::recursive_timed_mutex> shutdown_lock(shutdown_mutex_);
385 if (workers_.empty())
386 return;
387 std::queue<queued_task> discarded;
388 {
389 std::lock_guard<std::mutex> lock(queue_mutex_);
390 if (!stop_)
391 stop_ = true;
392 if (policy == shutdown_policy_backend::drop_pending && !tasks_.empty())
393 tasks_.swap(discarded);
394 }
395 shutdown_completed_all_ = shutdown_completed_all_ && discarded.empty();
396
397 condition_.notify_all();
398 task_finished_condition_.notify_all();
399 {
400 std::queue<queued_task> empty;
401 discarded.swap(empty);
402 }
403
404 for (auto& worker : workers_)
405 {
406 if (worker.joinable())
407 worker.join();
408 }
409
410 workers_.clear();
411 shutdown_completed_at_ = std::chrono::steady_clock::now();
412 }
413
424 auto
425 shutdown_for(std::chrono::milliseconds timeout) -> bool
426 {
427 if (is_current_worker())
429 auto const deadline = shutdown_deadline_after(timeout);
430 std::unique_lock<std::recursive_timed_mutex> shutdown_lock(shutdown_mutex_, std::defer_lock);
431 if (deadline == std::chrono::steady_clock::time_point::max())
432 shutdown_lock.lock();
433 else if (!shutdown_lock.try_lock_until(deadline))
434 return false;
435
436 std::unique_lock<std::mutex> lock(queue_mutex_);
437 if (workers_.empty())
438 return shutdown_completed_all_ && shutdown_completed_at_ <= deadline;
439 if (!stop_)
440 stop_ = true;
441 condition_.notify_all();
442 bool const drained = task_finished_condition_.wait_until(
443 lock, deadline, [this] { return tasks_.empty() && active_tasks_.load(std::memory_order_acquire) == 0; });
444 std::queue<queued_task> discarded;
445 if (!drained)
446 tasks_.swap(discarded);
447 shutdown_completed_all_ = shutdown_completed_all_ && discarded.empty();
448 lock.unlock();
449
450 condition_.notify_all();
451 task_finished_condition_.notify_all();
452 {
453 std::queue<queued_task> empty;
454 discarded.swap(empty);
455 }
456 if (deadline == std::chrono::steady_clock::time_point::max())
457 {
458 for (auto& worker : workers_)
459 if (worker.joinable())
460 worker.join();
461 workers_.clear();
462 }
463 if (drained && shutdown_completed_at_ == std::chrono::steady_clock::time_point{})
464 shutdown_completed_at_ = std::chrono::steady_clock::now();
465 return drained && shutdown_completed_all_;
466 }
467
469
472
474 [[nodiscard]] auto
476 {
477 auto const now = std::chrono::steady_clock::now();
478 auto const elapsed = std::chrono::duration_cast<std::chrono::seconds>(now - start_time_);
479
480 std::lock_guard<std::mutex> lock(queue_mutex_);
481 statistics stats;
482 stats.total_threads = num_threads_;
483 stats.active_threads = active_tasks_.load(std::memory_order_acquire);
484 stats.pending_tasks = tasks_.size();
485 stats.completed_tasks = completed_tasks_.load(std::memory_order_acquire);
486
487 if (elapsed.count() > 0)
488 {
489 stats.tasks_per_second = static_cast<double>(stats.completed_tasks) / elapsed.count();
490 }
491 else
492 {
493 stats.tasks_per_second = 0.0;
494 }
495
496 auto const total_task_time = total_task_time_.load(std::memory_order_acquire);
497 if (stats.completed_tasks > 0)
498 {
499 stats.avg_task_time = std::chrono::microseconds(total_task_time / stats.completed_tasks);
500 }
501 else
502 {
503 stats.avg_task_time = std::chrono::microseconds(0);
504 }
505
506 return stats;
507 }
508
510
513
518 void
520 {
521 std::lock_guard<std::mutex> lock(trace_mutex_);
522 on_task_start_ = task_start_callback_storage(std::move(cb));
523 }
524
525 template <typename Callback,
526 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, task_start_callback>, int> = 0>
527 void
528 set_on_task_start(Callback&& cb)
529 {
530 static_assert(std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id>,
531 "Task start callback must accept (time_point, std::thread::id)");
532 std::lock_guard<std::mutex> lock(trace_mutex_);
533 on_task_start_ = detail::make_copyable_function<void(std::chrono::steady_clock::time_point, std::thread::id)>(
534 std::forward<Callback>(cb));
535 }
536
542 void
544 {
545 std::lock_guard<std::mutex> lock(trace_mutex_);
546 on_task_end_ = task_end_callback_storage(std::move(cb));
547 }
548
549 template <typename Callback,
550 std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<Callback>, task_end_callback>, int> = 0>
551 void
552 set_on_task_end(Callback&& cb)
553 {
554 static_assert(std::is_invocable_r_v<void, Callback&, std::chrono::steady_clock::time_point, std::thread::id,
555 std::chrono::microseconds>,
556 "Task end callback must accept (time_point, std::thread::id, "
557 "std::chrono::microseconds)");
558 std::lock_guard<std::mutex> lock(trace_mutex_);
559 on_task_end_ = detail::make_copyable_function<void(std::chrono::steady_clock::time_point, std::thread::id,
560 std::chrono::microseconds)>(std::forward<Callback>(cb));
561 }
562
564
565private:
566 size_t num_threads_;
567 bool register_workers_;
569 std::vector<detail::thread_backend> workers_;
570 std::queue<queued_task> tasks_;
571
572 mutable std::mutex queue_mutex_;
573 std::condition_variable condition_;
574 std::condition_variable task_finished_condition_;
575 std::recursive_timed_mutex shutdown_mutex_;
576 std::atomic<bool> stop_;
577 bool shutdown_completed_all_{ true };
578 std::chrono::steady_clock::time_point shutdown_completed_at_{};
579 std::atomic<size_t> active_tasks_{ 0 };
580 std::atomic<size_t> completed_tasks_{ 0 };
581 std::atomic<uint64_t> total_task_time_{ 0 };
582
583 std::mutex trace_mutex_;
584 task_start_callback_storage on_task_start_;
585 task_end_callback_storage on_task_end_;
586
587 std::chrono::steady_clock::time_point start_time_;
588 inline static thread_local thread_pool_backend_base* current_pool = nullptr;
589
590 void
591 worker_function(size_t worker_id)
592 {
593 detail::worker_context_guard<thread_pool_backend_base> worker_context(current_pool, this);
594 std::optional<registration_guard_backend> reg_guard;
595 try
596 {
597 if (register_workers_)
598 reg_guard.emplace("pool_worker_" + std::to_string(worker_id), "threadschedule.pool");
599 startup_.arrive();
600 }
601 catch (...)
602 {
603 startup_.arrive(std::current_exception());
604 return;
605 }
606
607 while (true)
608 {
609 queued_task task;
610 bool found_task = false;
611
612 {
613 std::unique_lock<std::mutex> lock(queue_mutex_);
614
615 if (WaitPolicy::wait(condition_, lock, [this] { return stop_ || !tasks_.empty(); }))
616 {
617 if (stop_ && tasks_.empty())
618 {
619 return;
620 }
621
622 if (!tasks_.empty())
623 {
624 task = std::move(tasks_.front());
625 tasks_.pop();
626 found_task = true;
627 active_tasks_.fetch_add(1, std::memory_order_relaxed);
628 }
629 }
630 else if (stop_)
631 {
632 return;
633 }
634 }
635
636 if (found_task)
637 {
638 auto const start_time = std::chrono::steady_clock::now();
639 auto const tid = std::this_thread::get_id();
640
641 try
642 {
643 task_start_callback_storage on_task_start;
644 {
645 std::lock_guard<std::mutex> tl(trace_mutex_);
646 on_task_start = on_task_start_;
647 }
648 if (on_task_start)
649 on_task_start(start_time, tid);
650 }
651 catch (...)
652 {
653 }
654
655 // See work_stealing_pool_backend::worker_function for rationale.
656 try
657 {
658 task();
659 }
660 catch (...)
661 {
662 }
663 auto const end_time = std::chrono::steady_clock::now();
664
665 auto const task_duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
666 total_task_time_.fetch_add(task_duration.count(), std::memory_order_relaxed);
667
668 try
669 {
670 task_end_callback_storage on_task_end;
671 {
672 std::lock_guard<std::mutex> tl(trace_mutex_);
673 on_task_end = on_task_end_;
674 }
675 if (on_task_end)
676 on_task_end(end_time, tid, task_duration);
677 }
678 catch (...)
679 {
680 }
681
682 {
683 std::lock_guard<std::mutex> lock(queue_mutex_);
684 active_tasks_.fetch_sub(1, std::memory_order_relaxed);
685 }
686 completed_tasks_.fetch_add(1, std::memory_order_relaxed);
687
688 task_finished_condition_.notify_all();
689 }
690 }
691 }
692};
693
705
716
717} // namespace threadschedule::detail
Manages a set of CPU indices to which a thread may be bound.
Definition native.hpp:433
Value-semantic wrapper for a thread scheduling priority.
Definition native.hpp:140
static constexpr auto normal() noexcept -> native_thread_priority
Definition native.hpp:164
Single-queue thread pool parameterized by its idle-wait strategy.
void post(F &&f, Args &&... args)
Fire-and-forget task submission (throwing variant).
auto size() const noexcept -> size_t
Number of worker threads.
auto submit_batch(Iterator begin, Iterator end) -> std::vector< std::future< void > >
Submit a batch of tasks (throwing).
auto submit(F &&f, Args &&... args) -> std::future< bind_result_t< F, Args... > >
Submit a task, throwing on shutdown.
auto try_post(F &&f, Args &&... args) -> expected< void, std::error_code >
Fire-and-forget task submission (non-throwing variant).
thread_pool_backend_base(size_t num_threads=default_worker_count(), bool register_workers=false)
auto set_affinity(native_thread_affinity const &affinity) -> expected< void, std::error_code >
Pin all workers to the same CPU set.
auto operator=(thread_pool_backend_base const &) -> thread_pool_backend_base &=delete
void set_on_task_start(task_start_callback cb)
Register a callback invoked just before each task executes.
void parallel_for_each(Iterator begin, Iterator end, F &&func)
Apply func to [begin, end) in parallel (chunked).
auto get_statistics() const -> statistics
Collect approximate performance counters.
thread_pool_backend_base(thread_pool_backend_base const &)=delete
auto configure_threads(std::string const &name_prefix, native_scheduling_policy policy=native_scheduling_policy::other, native_thread_priority priority=native_thread_priority::normal()) -> expected< void, std::error_code >
Name, schedule and prioritize all worker threads.
auto shutdown_for(std::chrono::milliseconds timeout) -> bool
Attempt a timed drain: finish as many tasks as possible within timeout, then discard queued work.
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).
void wait_for_tasks()
Block until all pending and active tasks have completed.
void shutdown(shutdown_policy_backend policy=shutdown_policy_backend::drain)
Shut the pool down.
auto pending_tasks() const -> size_t
Number of tasks waiting in the queue.
void set_on_task_end(task_end_callback cb)
Register a callback invoked just after each task completes.
auto try_submit(F &&f, Args &&... args) -> expected< std::future< bind_result_t< F, Args... > >, std::error_code >
Submit a task without throwing on shutdown.
auto configure_threads(native_thread_config const &config) -> expected< void, std::error_code >
auto distribute_across_cpus() -> expected< void, std::error_code >
Pin each worker to a distinct CPU core (round-robin).
constexpr auto has_value() const noexcept -> bool
Definition expected.hpp:599
Blocking idle-wait strategy for queue-based pools.
Aggregates multiple thread_registry_backend instances into a single queryable view.
auto configure_worker_threads(WorkerRange &workers, std::string const &name_prefix, native_scheduling_policy policy, native_thread_priority priority, thread_registry_backend *registry=nullptr) -> expected< void, std::error_code >
native_scheduling_policy
Enumeration of available thread scheduling policies.
Definition native.hpp:85
@ other
Standard round-robin time-sharing.
auto distribute_workers_across_cpus(WorkerRange &workers) -> expected< void, std::error_code >
task_start_callback task_start_callback_storage
Definition callbacks.hpp:15
auto multipass_range_size(Iterator begin, Iterator end) -> size_t
std::invoke_result_t< decltype(bind_args(std::declval< F >(), std::declval< Args >()...))& > bind_result_t
Result of invoking the decayed callable and arguments stored by bind_args.
Definition bind.hpp:32
task_end_callback task_end_callback_storage
Definition callbacks.hpp:16
copyable_function< void(std::chrono::steady_clock::time_point, std::thread::id)> task_start_callback
Definition callbacks.hpp:11
auto default_worker_count() noexcept -> std::size_t
auto bind_args(F &&function, Args &&... args)
Definition bind.hpp:17
auto checked_worker_count(std::size_t count) -> std::size_t
auto make_copyable_function(Callable &&callable) -> copyable_function< Signature >
auto set_worker_affinity(WorkerRange &workers, native_thread_affinity const &affinity) -> expected< void, std::error_code >
copyable_function< void(std::chrono::steady_clock::time_point, std::thread::id, std::chrono::microseconds elapsed)> task_end_callback
Definition callbacks.hpp:13
auto runtime_registry() -> thread_registry_backend &
void parallel_for_each_chunked(Pool &pool, Iterator begin, Iterator end, F &&func, size_t num_workers)
auto shutdown_deadline_after(std::chrono::milliseconds timeout) -> std::chrono::steady_clock::time_point
Definition deadline.hpp:11
expected< T, std::error_code > result
Standard result type used by public APIs.
Definition result.hpp:22
Timed idle-wait strategy for queue-based pools.
Internal queued-task shutdown behavior.
Worker identity, CPU selection, and registration helpers.
Worker-thread count configuration for pool types.