ThreadSchedule 3.0.0
Modern C++ thread management library
Loading...
Searching...
No Matches
scheduled_pool_backend_base.hpp
Go to the documentation of this file.
1#pragma once
2
7#include "../pool/backend.hpp"
8#include "../pool/worker_count.hpp"
9#include "../time.hpp"
11
12#include <atomic>
13#include <chrono>
14#include <condition_variable>
15#include <cstddef>
16#include <cstdint>
17#include <functional>
18#include <future>
19#include <limits>
20#include <map>
21#include <memory>
22#include <mutex>
23#include <optional>
24#include <stdexcept>
25#include <thread>
26#include <type_traits>
27#include <utility>
28
30{
31
103template <typename PoolType = thread_pool_backend>
105{
106public:
107 using task_type = std::function<void()>;
110 using time_point = std::chrono::steady_clock::time_point;
111 using duration = std::chrono::steady_clock::duration;
112
114 {
115 explicit periodic_task_state(periodic_task_type task_value) : task(std::move(task_value)) {}
116
118 std::atomic<bool> running{ false };
119 };
120
122 {
123 explicit scheduled_dispatch_state(std::shared_ptr<detail::scheduled_cancellation_state> cancellation_value,
124 std::shared_ptr<periodic_task_state> periodic_value = {})
125 : cancellation(std::move(cancellation_value)), periodic(std::move(periodic_value))
126 {
127 }
128
130 {
131 if (started.load(std::memory_order_acquire))
132 return;
133 cancellation->pool_stopped.store(true, std::memory_order_release);
134 if (periodic)
135 periodic->running.store(false, std::memory_order_release);
136 }
137
138 std::shared_ptr<detail::scheduled_cancellation_state> cancellation;
139 std::shared_ptr<periodic_task_state> periodic;
140 std::atomic<bool> started{ false };
141 };
142
144 {
145 public:
146 enum class kind : std::uint8_t
147 {
148 one_shot,
150 };
151
152 [[nodiscard]] static auto
153 one_shot(std::uint64_t id, time_point run_time, one_shot_task_type task,
154 std::shared_ptr<detail::scheduled_cancellation_state> cancellation) -> scheduled_task_info
155 {
156 return scheduled_task_info(id, run_time, std::move(task), std::move(cancellation));
157 }
158
159 [[nodiscard]] static auto
160 periodic(std::uint64_t id, time_point run_time, duration interval, periodic_task_type task,
161 std::shared_ptr<detail::scheduled_cancellation_state> cancellation) -> scheduled_task_info
162 {
163 if (interval <= duration::zero())
164 throw std::invalid_argument("scheduled_pool periodic interval must be positive");
165 return scheduled_task_info(id, run_time, interval, std::move(task), std::move(cancellation));
166 }
167
168 [[nodiscard]] auto
169 type() const noexcept -> kind
170 {
171 return kind_;
172 }
173 [[nodiscard]] auto
174 cancellation() const noexcept -> std::shared_ptr<detail::scheduled_cancellation_state> const&
175 {
176 return cancellation_;
177 }
178 [[nodiscard]] auto
179 periodic_state() const noexcept -> std::shared_ptr<periodic_task_state> const&
180 {
181 return periodic_task_;
182 }
183 [[nodiscard]] auto
185 {
186 return std::move(one_shot_task_);
187 }
188 [[nodiscard]] auto
189 next_run() const noexcept -> time_point
190 {
191 return next_run_;
192 }
193
194 void
196 {
197 auto const interval = *interval_;
198 auto advanced = detail::checked_deadline_after(next_run_, interval);
199 if (!advanced)
200 throw std::system_error(advanced.error(), "scheduled_pool periodic deadline");
201 next_run_ = advanced.value();
202 if (next_run_ <= now)
203 {
204 auto const quotient = (now - next_run_) / interval;
205 auto const maximum = (std::numeric_limits<decltype(quotient)>::max)();
206 if (quotient == maximum)
207 throw std::system_error(std::make_error_code(std::errc::value_too_large),
208 "scheduled_pool periodic deadline");
209 auto const missed = quotient + 1;
210 if (missed > maximum / interval.count())
211 throw std::system_error(std::make_error_code(std::errc::value_too_large),
212 "scheduled_pool periodic deadline");
213 advanced = detail::checked_deadline_after(next_run_, interval * missed);
214 if (!advanced)
215 throw std::system_error(advanced.error(), "scheduled_pool periodic deadline");
216 next_run_ = advanced.value();
217 }
218 }
219
220 private:
221 scheduled_task_info(std::uint64_t id, time_point run_time, one_shot_task_type task,
222 std::shared_ptr<detail::scheduled_cancellation_state> cancellation)
223 : id_(id), next_run_(run_time), one_shot_task_(std::move(task)), cancellation_(std::move(cancellation)),
224 kind_(kind::one_shot)
225 {
226 }
227
228 scheduled_task_info(std::uint64_t id, time_point run_time, duration interval, periodic_task_type task,
229 std::shared_ptr<detail::scheduled_cancellation_state> cancellation)
230 : id_(id), next_run_(run_time), interval_(interval),
231 periodic_task_(std::make_shared<periodic_task_state>(std::move(task))),
232 cancellation_(std::move(cancellation)), kind_(kind::periodic)
233 {
234 }
235
236 std::uint64_t id_;
237 time_point next_run_;
238 std::optional<duration> interval_;
239 one_shot_task_type one_shot_task_;
240 std::shared_ptr<periodic_task_state> periodic_task_;
241 std::shared_ptr<detail::scheduled_cancellation_state> cancellation_;
242 kind kind_;
243 };
244
250 explicit scheduled_pool_backend_base(size_t worker_threads = default_worker_count())
251 : pool_(worker_threads), stop_(false), next_task_id_(1)
252 {
253 start_scheduler();
254 }
255
256 template <typename T = PoolType, std::enable_if_t<std::is_constructible_v<T, size_t, bool>, int> = 0>
257 scheduled_pool_backend_base(size_t worker_threads, bool register_workers)
258 : pool_(worker_threads, register_workers), stop_(false), next_task_id_(1)
259 {
260 start_scheduler();
261 }
262
265
270
277 auto
279 {
280 auto run_time = detail::checked_deadline_after(std::chrono::steady_clock::now(), delay);
281 if (!run_time)
282 throw std::system_error(run_time.error(), "scheduled_pool delay");
283 return insert_one_shot_task(run_time.value(), detail::make_move_only_function<void()>(std::move(task)));
284 }
285
286 template <typename F, std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<F>, task_type>, int> = 0>
287 auto
289 {
290 auto run_time = detail::checked_deadline_after(std::chrono::steady_clock::now(), delay);
291 if (!run_time)
292 throw std::system_error(run_time.error(), "scheduled_pool delay");
293 return insert_one_shot_task(run_time.value(), detail::make_move_only_function<void()>(std::forward<F>(task)));
294 }
295
302 auto
304 {
305 return insert_one_shot_task(time_point, detail::make_move_only_function<void()>(std::move(task)));
306 }
307
308 template <typename F, std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<F>, task_type>, int> = 0>
309 auto
311 {
312 return insert_one_shot_task(time_point, detail::make_move_only_function<void()>(std::forward<F>(task)));
313 }
314
324 auto
326 {
327 return schedule_periodic_after(duration::zero(), interval, std::move(task));
328 }
329
330 template <typename F, std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<F>, task_type>, int> = 0>
331 auto
333 {
334 return schedule_periodic_after(duration::zero(), interval, std::forward<F>(task));
335 }
336
344 auto
346 {
347 if (interval <= duration::zero())
348 throw std::invalid_argument("scheduled_pool periodic interval must be positive");
349 auto const run_time = detail::checked_deadline_after(std::chrono::steady_clock::now(), initial_delay);
350 if (!run_time)
351 throw std::system_error(run_time.error(), "scheduled_pool initial delay");
352 auto const next_run = detail::checked_deadline_after(run_time.value(), interval);
353 if (!next_run)
354 throw std::system_error(next_run.error(), "scheduled_pool periodic deadline");
355 return insert_periodic_task(run_time.value(), interval, detail::make_move_only_function<void()>(std::move(task)));
356 }
357
358 template <typename F, std::enable_if_t<!std::is_same_v<detail::remove_cvref_t<F>, task_type>, int> = 0>
359 auto
361 {
362 if (interval <= duration::zero())
363 throw std::invalid_argument("scheduled_pool periodic interval must be positive");
364 auto const run_time = detail::checked_deadline_after(std::chrono::steady_clock::now(), initial_delay);
365 if (!run_time)
366 throw std::system_error(run_time.error(), "scheduled_pool initial delay");
367 auto const next_run = detail::checked_deadline_after(run_time.value(), interval);
368 if (!next_run)
369 throw std::system_error(next_run.error(), "scheduled_pool periodic deadline");
370 return insert_periodic_task(run_time.value(), interval,
371 detail::make_move_only_function<void()>(std::forward<F>(task)));
372 }
373
380 static void
382 {
383 handle.cancel();
384 }
385
389 [[nodiscard]] auto
390 scheduled_count() const -> size_t
391 {
392 std::lock_guard<std::mutex> lock(mutex_);
393 return scheduled_tasks_.size();
394 }
395
396 [[nodiscard]] auto
397 is_current_context() const noexcept -> bool
398 {
399 return pool_.is_current_worker() || current_scheduler == this;
400 }
401
405 [[nodiscard]] auto
406 thread_pool() -> PoolType&
407 {
408 return pool_;
409 }
410
414 void
416 {
417 if (is_current_context())
419 std::lock_guard<std::recursive_mutex> shutdown_lock(shutdown_mutex_);
420 std::multimap<time_point, scheduled_task_info> discarded;
421 {
422 std::lock_guard<std::mutex> lock(mutex_);
423 if (stop_)
424 return;
425 stop_ = true;
426 for (auto const& task : scheduled_tasks_)
427 task.second.cancellation()->pool_stopped.store(true, std::memory_order_release);
428 scheduled_tasks_.swap(discarded);
429 }
430
431 discarded.clear();
432
433 condition_.notify_one();
434
435 {
436 std::lock_guard<std::mutex> scheduler_lock(scheduler_mutex_);
437 if (scheduler_thread_.joinable())
438 {
439 scheduler_thread_.join();
440 scheduler_tid_ = native_thread_id{};
441 }
442 }
443
444 pool_.shutdown(policy);
445 }
446
452 auto
455 {
456 return pool_.configure_threads(name_prefix, policy, priority);
457 }
458
459 auto
461 {
462 return pool_.configure_threads(config);
463 }
464
465 [[nodiscard]] auto
467 {
468 if (current_scheduler == this)
470 std::lock_guard<std::mutex> scheduler_lock(scheduler_mutex_);
471 if (!scheduler_thread_.joinable() || scheduler_tid_ == native_thread_id{})
472 return std::nullopt;
473 return thread_info(scheduler_tid_);
474 }
475
476 auto
480 {
481 auto info = scheduler_thread_info();
482 if (!info.has_value())
483 return unexpected(std::make_error_code(std::errc::no_such_process));
484 return detail::configure_thread(info.value(), name, policy, priority);
485 }
486
487 auto
489 {
490 auto info = scheduler_thread_info();
491 if (!info.has_value())
492 return unexpected(std::make_error_code(std::errc::no_such_process));
493 return info->configure(config);
494 }
495
496private:
497 PoolType pool_;
498 detail::thread_backend scheduler_thread_;
499 native_thread_id scheduler_tid_{};
500
501 mutable std::mutex mutex_;
502 std::condition_variable condition_;
503 std::atomic<bool> stop_;
504 std::recursive_mutex shutdown_mutex_;
505 mutable std::mutex scheduler_mutex_;
506
507 std::multimap<time_point, scheduled_task_info> scheduled_tasks_;
508 std::atomic<uint64_t> next_task_id_;
509
510 inline static thread_local scheduled_pool_backend_base* current_scheduler = nullptr;
511
512 void
513 start_scheduler()
514 {
515 std::promise<native_thread_id> scheduler_started;
516 auto scheduler_ready = scheduler_started.get_future();
517
518 scheduler_thread_ = detail::thread_backend(
519 [this, started = std::move(scheduler_started)]() mutable
520 {
521 current_scheduler = this;
522 started.set_value(thread_info::get_thread_id());
523 scheduler_loop();
524 current_scheduler = nullptr;
525 });
526
527 scheduler_tid_ = scheduler_ready.get();
528 (void)thread_info(scheduler_tid_).set_name("ts_sched_pool");
529 }
530
531 auto
532 insert_one_shot_task(time_point run_time, one_shot_task_type task) -> scheduled_task_backend
533 {
534 std::lock_guard<std::mutex> lock(mutex_);
535
536 uint64_t const task_id = next_task_id_++;
537 scheduled_task_backend handle(task_id);
538
539 if (stop_)
540 {
541 handle.cancel();
542 return handle;
543 }
544
545 scheduled_tasks_.insert(
546 { run_time, scheduled_task_info::one_shot(task_id, run_time, std::move(task), handle.get_cancellation()) });
547 condition_.notify_one();
548
549 return handle;
550 }
551
552 auto
553 insert_periodic_task(time_point run_time, duration interval, periodic_task_type task) -> scheduled_task_backend
554 {
555 std::lock_guard<std::mutex> lock(mutex_);
556
557 uint64_t const task_id = next_task_id_++;
558 scheduled_task_backend handle(task_id);
559
560 if (stop_)
561 {
562 handle.cancel();
563 return handle;
564 }
565
566 scheduled_tasks_.insert({ run_time, scheduled_task_info::periodic(task_id, run_time, interval, std::move(task),
567 handle.get_cancellation()) });
568 condition_.notify_one();
569
570 return handle;
571 }
572
573 void
574 scheduler_loop()
575 {
576 while (true)
577 {
578 std::unique_lock<std::mutex> lock(mutex_);
579
580 if (stop_)
581 return;
582
583 // Wait until we have tasks or need to stop
584 if (scheduled_tasks_.empty())
585 {
586 condition_.wait(lock, [this] { return stop_ || !scheduled_tasks_.empty(); });
587
588 if (stop_)
589 return;
590 }
591
592 // Get the next task to execute
593 auto const now = std::chrono::steady_clock::now();
594 auto it = scheduled_tasks_.begin();
595
596 if (it == scheduled_tasks_.end())
597 {
598 continue;
599 }
600
601 // Wait until it's time to execute
602 if (it->first > now)
603 {
604 // wait_until releases the mutex while retaining a reference to
605 // its deadline. Shutdown may clear scheduled_tasks_ during that
606 // wait, so never pass a map key by reference here.
607 auto const next_run = it->first;
608 condition_.wait_until(lock, next_run);
609
610 if (stop_)
611 return;
612
613 continue;
614 }
615
616 // Extract the task info
617 scheduled_task_info info = std::move(it->second);
618 scheduled_tasks_.erase(it);
619 lock.unlock();
620
621 // Check if cancelled
622 if (info.cancellation()->user_cancelled.load(std::memory_order_acquire)
623 || info.cancellation()->pool_stopped.load(std::memory_order_acquire))
624 {
625 continue;
626 }
627
628 // Schedule for execution in the thread pool
629 try
630 {
631 auto cancellation = info.cancellation();
632 if (info.type() == scheduled_task_info::kind::periodic)
633 {
634 auto periodic_task = info.periodic_state();
635 bool const already_running = periodic_task->running.exchange(true, std::memory_order_acq_rel);
636 if (!already_running)
637 {
638 auto dispatch = std::make_shared<scheduled_dispatch_state>(cancellation, periodic_task);
639 try
640 {
641 pool_.post(
642 [periodic_task, cancellation, dispatch]()
643 {
644 dispatch->started.store(true, std::memory_order_release);
645 try
646 {
647 if (!cancellation->user_cancelled.load(std::memory_order_acquire))
648 periodic_task->task();
649 }
650 catch (...)
651 {
652 periodic_task->running.store(false, std::memory_order_release);
653 throw;
654 }
655 periodic_task->running.store(false, std::memory_order_release);
656 });
657 }
658 catch (...)
659 {
660 periodic_task->running.store(false, std::memory_order_release);
661 throw;
662 }
663 }
664
665 info.advance(std::chrono::steady_clock::now());
666 std::lock_guard<std::mutex> schedule_lock(mutex_);
667 if (!stop_ && !cancellation->user_cancelled.load(std::memory_order_acquire))
668 scheduled_tasks_.insert({ info.next_run(), std::move(info) });
669 }
670 else
671 {
672 auto one_shot_task = info.take_one_shot();
673 auto dispatch = std::make_shared<scheduled_dispatch_state>(cancellation);
674 pool_.post(
675 [task = std::move(one_shot_task), cancellation, dispatch]() mutable
676 {
677 dispatch->started.store(true, std::memory_order_release);
678 if (!cancellation->user_cancelled.load(std::memory_order_acquire))
679 {
680 task();
681 }
682 });
683 }
684 }
685 catch (...)
686 {
687 info.cancellation()->pool_stopped.store(true, std::memory_order_release);
688 // Thread pool might be shutting down
689 }
690 }
691 }
692};
693
708
709} // namespace threadschedule::detail
Value-semantic wrapper for a thread scheduling priority.
Definition native.hpp:140
static constexpr auto normal() noexcept -> native_thread_priority
Definition native.hpp:164
auto periodic_state() const noexcept -> std::shared_ptr< periodic_task_state > const &
auto cancellation() const noexcept -> std::shared_ptr< detail::scheduled_cancellation_state > const &
static auto one_shot(std::uint64_t id, time_point run_time, one_shot_task_type task, std::shared_ptr< detail::scheduled_cancellation_state > cancellation) -> scheduled_task_info
static auto periodic(std::uint64_t id, time_point run_time, duration interval, periodic_task_type task, std::shared_ptr< detail::scheduled_cancellation_state > cancellation) -> scheduled_task_info
Thread pool augmented with delayed and periodic task scheduling.
auto operator=(scheduled_pool_backend_base const &) -> scheduled_pool_backend_base &=delete
auto schedule_at(time_point time_point, F &&task) -> scheduled_task_backend
scheduled_pool_backend_base(size_t worker_threads, bool register_workers)
auto configure_scheduler_thread(std::string const &name, native_scheduling_policy policy=native_scheduling_policy::other, native_thread_priority priority=native_thread_priority::normal()) -> expected< void, std::error_code >
auto scheduler_thread_info() const -> std::optional< thread_info >
static void cancel(scheduled_task_backend &handle)
Cancel a scheduled task by handle.
auto schedule_periodic_after(duration initial_delay, duration interval, F &&task) -> scheduled_task_backend
void shutdown(shutdown_policy_backend policy=shutdown_policy_backend::drain)
Shutdown the scheduler and wait for completion.
auto schedule_after(duration delay, F &&task) -> scheduled_task_backend
auto schedule_periodic(duration interval, F &&task) -> scheduled_task_backend
auto configure_scheduler_thread(native_thread_config const &config) -> expected< void, std::error_code >
auto configure_threads(native_thread_config const &config) -> expected< void, std::error_code >
auto thread_pool() -> PoolType &
Get the underlying thread pool for direct task submission.
scheduled_pool_backend_base(scheduled_pool_backend_base const &)=delete
auto scheduled_count() const -> size_t
Get number of scheduled tasks (including periodic)
auto schedule_at(time_point time_point, task_type task) -> scheduled_task_backend
Schedule a task to run at a specific time point.
auto schedule_periodic_after(duration initial_delay, duration interval, task_type task) -> scheduled_task_backend
Schedule a task to run periodically after an initial delay.
auto schedule_after(duration delay, task_type task) -> scheduled_task_backend
Schedule a task to run after a delay.
scheduled_pool_backend_base(size_t worker_threads=default_worker_count())
Create a scheduled thread pool.
auto configure_threads(std::string const &name_prefix, native_scheduling_policy policy=native_scheduling_policy::other, native_thread_priority priority=native_thread_priority::normal())
Configure worker threads.
auto schedule_periodic(duration interval, task_type task) -> scheduled_task_backend
Schedule a task to run periodically at fixed intervals.
Copyable handle for a cancellable scheduled task.
Owning wrapper around std::thread with RAII join-on-destroy semantics.
Lightweight handle for querying and controlling a specific OS thread.
static auto get_thread_id() -> native_thread_id
Aggregates multiple thread_registry_backend instances into a single queryable view.
native_scheduling_policy
Enumeration of available thread scheduling policies.
Definition native.hpp:85
@ other
Standard round-robin time-sharing.
auto configure_thread(ThreadLike &thread, std::string const &name, native_scheduling_policy policy, native_thread_priority priority) -> expected< void, std::error_code >
auto make_move_only_function(Callable &&callable) -> move_only_function< Signature, InlineSize >
auto default_worker_count() noexcept -> std::size_t
auto checked_deadline_after(std::chrono::time_point< Clock, Duration > now, Duration delay) -> expected< std::chrono::time_point< Clock, Duration >, std::error_code >
Definition time.hpp:50
Internal cancellable scheduled-task handle.
scheduled_dispatch_state(std::shared_ptr< detail::scheduled_cancellation_state > cancellation_value, std::shared_ptr< periodic_task_state > periodic_value={})