Time Shield Library
C++ library for working with time
Loading...
Searching...
No Matches
TimerScheduler.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2#pragma once
3#ifndef _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED
4#define _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED
5
15
16#include "config.hpp"
17
18#include <atomic>
19#include <cassert>
20#include <chrono>
21#include <condition_variable>
22#include <cstddef>
23#include <cstdint>
24#include <functional>
25#include <memory>
26#include <mutex>
27#include <queue>
28#include <thread>
29#include <unordered_map>
30#include <utility>
31#include <vector>
32
33namespace time_shield {
34
35 class TimerScheduler;
36 class Timer;
37
38 namespace detail {
39
40 using TimerClock = std::chrono::steady_clock;
41 using TimerCallback = std::function<void()>;
42
44 struct TimerState {
46 std::mutex m_callback_mutex;
48 std::atomic<std::int64_t> m_interval_ms{0};
49 std::atomic<bool> m_is_single_shot{false};
50 std::atomic<bool> m_is_active{false};
51 std::atomic<bool> m_is_running{false};
52 std::size_t m_id{0};
53 std::atomic<std::uint64_t> m_generation{0};
54 std::atomic<bool> m_has_external_owner{false};
55 };
56
58 static thread_local TimerState* state = nullptr;
59 return state;
60 }
61
75
78 ScheduledTimer() = default;
79
80 ScheduledTimer(TimerClock::time_point fire_time, std::size_t timer_id, std::uint64_t generation)
81 : m_fire_time(fire_time), m_timer_id(timer_id), m_generation(generation) {}
82
83 TimerClock::time_point m_fire_time{};
84 std::size_t m_timer_id{0};
85 std::uint64_t m_generation{0};
86 };
87
90 bool operator()(const ScheduledTimer& lhs, const ScheduledTimer& rhs) const {
91 return lhs.m_fire_time > rhs.m_fire_time;
92 }
93 };
94
96 struct DueTimer {
97 DueTimer() = default;
98
99 DueTimer(TimerClock::time_point fire_time,
100 std::uint64_t generation,
101 std::shared_ptr<TimerState> state)
102 : m_fire_time(fire_time), m_generation(generation), m_state(std::move(state)) {}
103
104 TimerClock::time_point m_fire_time{};
105 std::uint64_t m_generation{0};
106 std::shared_ptr<TimerState> m_state;
107 };
108
109 } // namespace detail
110
111 using timer_state_ptr = std::shared_ptr<detail::TimerState>;
112
115 public:
117
120
125
132 void run();
133
135 void stop();
136
142 void process();
143
145 void update();
146
150 std::size_t active_timer_count_for_testing();
151
152 private:
153 friend class Timer;
154
156 void destroy_timer_state(const timer_state_ptr& state);
157 void start_timer(const timer_state_ptr& state, clock::time_point when);
158 void stop_timer(const timer_state_ptr& state);
159
160 void worker_loop();
161 void collect_due_timers_locked(std::vector<detail::DueTimer>& due, clock::time_point now);
162 void execute_due_timers(std::vector<detail::DueTimer>& due);
163 void finalize_timer(const detail::DueTimer& due_timer);
164
165 std::mutex m_mutex;
166 std::condition_variable m_cv;
167 std::thread m_thread;
169 bool m_stop_requested{false};
170 std::priority_queue<detail::ScheduledTimer, std::vector<detail::ScheduledTimer>, detail::ScheduledComparator> m_queue;
171 std::unordered_map<std::size_t, std::weak_ptr<detail::TimerState>> m_timers;
172 std::size_t m_next_id{1};
173 };
174
176 class Timer {
177 public:
179
180 explicit Timer(TimerScheduler& scheduler);
181 ~Timer();
182
183 Timer(const Timer&) = delete;
184 Timer& operator=(const Timer&) = delete;
185 Timer(Timer&&) = delete;
186 Timer& operator=(Timer&&) = delete;
187
192 template<class Rep, class Period>
193 void set_interval(std::chrono::duration<Rep, Period> interval) noexcept;
194
196 std::chrono::milliseconds interval() const noexcept;
197
199 void start();
200
202 template<class Rep, class Period>
203 void start(std::chrono::duration<Rep, Period> interval);
204
210 void stop();
211
215 void stop_and_wait();
216
218 void set_single_shot(bool is_single_shot) noexcept;
219
221 bool is_single_shot() const noexcept;
222
224 bool is_active() const noexcept;
225
227 bool is_running() const noexcept;
228
230 void set_callback(Callback callback);
231
235 template<class Rep, class Period>
236 static void single_shot(TimerScheduler& scheduler,
237 std::chrono::duration<Rep, Period> interval,
238 Callback callback);
239
240 private:
243 };
244
245 // ---------------------------------------------------------------------
246 // TimerScheduler inline implementation
247 // ---------------------------------------------------------------------
248
249 inline TimerScheduler::TimerScheduler() = default;
250
252 stop();
253 std::lock_guard<std::mutex> lock(m_mutex);
254 for (auto& entry : m_timers) {
255 if (auto state = entry.second.lock()) {
256 std::lock_guard<std::mutex> callback_lock(state->m_callback_mutex);
257 state->m_callback = {};
258 }
259 }
260 m_timers.clear();
261 while (!m_queue.empty()) {
262 m_queue.pop();
263 }
264 }
265
266 inline void TimerScheduler::run() {
267 std::lock_guard<std::mutex> lock(m_mutex);
269 return;
270 }
271 m_stop_requested = false;
272 m_is_worker_running = true;
273 m_thread = std::thread(&TimerScheduler::worker_loop, this);
274 }
275
276 inline void TimerScheduler::stop() {
277 std::vector<timer_state_ptr> orphan_states;
278 std::thread worker_to_join;
279
280 {
281 std::unique_lock<std::mutex> lock(m_mutex);
283 m_stop_requested = true;
284 m_cv.notify_all();
285 worker_to_join = std::move(m_thread);
286 } else {
287 m_stop_requested = false;
288 }
289
290 for (auto it = m_timers.begin(); it != m_timers.end();) {
291 auto state = it->second.lock();
292 if (!state) {
293 it = m_timers.erase(it);
294 continue;
295 }
296
297 if (!state->m_has_external_owner.load(std::memory_order_relaxed)) {
298 orphan_states.push_back(state);
299 it = m_timers.erase(it);
300 } else {
301 ++it;
302 }
303 }
304 }
305
306 if (worker_to_join.joinable()) {
307 worker_to_join.join();
308 }
309
310 {
311 std::lock_guard<std::mutex> lock(m_mutex);
312 m_is_worker_running = false;
313 m_stop_requested = false;
314 }
315
316 for (auto& state : orphan_states) {
317 if (!state) {
318 continue;
319 }
320 std::lock_guard<std::mutex> callback_lock(state->m_callback_mutex);
321 state->m_callback = {};
322 state->m_is_active.store(false, std::memory_order_relaxed);
323 }
324 }
325
327 std::vector<detail::DueTimer> due;
328 {
329 std::lock_guard<std::mutex> lock(m_mutex);
330 assert(!m_is_worker_running && "process() must not be called while the worker thread is active");
331 const auto now = clock::now();
333 }
335 }
336
338 process();
339 }
340
342 std::lock_guard<std::mutex> lock(m_mutex);
343 std::size_t count = 0;
344 for (const auto& entry : m_timers) {
345 if (!entry.second.expired()) {
346 ++count;
347 }
348 }
349 return count;
350 }
351
353 auto state = std::make_shared<detail::TimerState>();
354 state->m_scheduler = this;
355 std::lock_guard<std::mutex> lock(m_mutex);
356 state->m_id = m_next_id++;
357 m_timers[state->m_id] = state;
358 return state;
359 }
360
362 if (!state) {
363 return;
364 }
365 {
366 std::lock_guard<std::mutex> callback_lock(state->m_callback_mutex);
367 state->m_callback = {};
368 }
369 std::lock_guard<std::mutex> lock(m_mutex);
370 state->m_is_active.store(false, std::memory_order_relaxed);
371 state->m_generation.fetch_add(1, std::memory_order_relaxed);
372 if (state->m_id != 0) {
373 m_timers.erase(state->m_id);
374 }
375 state->m_scheduler = nullptr;
376 }
377
378 inline void TimerScheduler::start_timer(const timer_state_ptr& state, clock::time_point when) {
379 if (!state) {
380 return;
381 }
382 std::lock_guard<std::mutex> lock(m_mutex);
383 state->m_is_active.store(true, std::memory_order_relaxed);
384 const auto generation = state->m_generation.fetch_add(1, std::memory_order_relaxed) + 1;
385 m_queue.push(detail::ScheduledTimer{when, state->m_id, generation});
386 m_cv.notify_all();
387 }
388
389 inline void TimerScheduler::stop_timer(const timer_state_ptr& state) {
390 if (!state) {
391 return;
392 }
393 std::lock_guard<std::mutex> lock(m_mutex);
394 state->m_is_active.store(false, std::memory_order_relaxed);
395 state->m_generation.fetch_add(1, std::memory_order_relaxed);
396 m_cv.notify_all();
397 }
398
400 std::vector<detail::DueTimer> due;
401 std::unique_lock<std::mutex> lock(m_mutex);
402 while (!m_stop_requested) {
403 if (m_queue.empty()) {
404 m_cv.wait(lock, [this] { return m_stop_requested || !m_queue.empty(); });
405 continue;
406 }
407
408 const auto next_fire_time = m_queue.top().m_fire_time;
409 const bool woke_by_condition = m_cv.wait_until(
410 lock,
411 next_fire_time,
412 [this, next_fire_time] {
413 return m_stop_requested || m_queue.empty() || m_queue.top().m_fire_time < next_fire_time;
414 }
415 );
416
417 if (m_stop_requested) {
418 break;
419 }
420
421 if (woke_by_condition) {
422 continue;
423 }
424
425 const auto now = clock::now();
427
428 lock.unlock();
430 due.clear();
431 lock.lock();
432 }
433 }
434
435 inline void TimerScheduler::collect_due_timers_locked(std::vector<detail::DueTimer>& due, clock::time_point now) {
436 while (!m_queue.empty()) {
437 const auto& top = m_queue.top();
438 if (top.m_fire_time > now) {
439 break;
440 }
441
442 detail::ScheduledTimer item = top;
443 m_queue.pop();
444
445 auto it = m_timers.find(item.m_timer_id);
446 if (it == m_timers.end()) {
447 continue;
448 }
449
450 auto state = it->second.lock();
451 if (!state) {
452 m_timers.erase(it);
453 continue;
454 }
455
456 if (!state->m_is_active.load(std::memory_order_relaxed) ||
457 state->m_generation.load(std::memory_order_relaxed) != item.m_generation) {
458 continue;
459 }
460
461 state->m_is_running.store(true, std::memory_order_release);
462 due.push_back(detail::DueTimer{item.m_fire_time, item.m_generation, std::move(state)});
463 }
464 }
465
466 inline void TimerScheduler::execute_due_timers(std::vector<detail::DueTimer>& due) {
467 for (auto& timer : due) {
468 detail::TimerCallback callback;
469 if (timer.m_state) {
470 std::lock_guard<std::mutex> callback_lock(timer.m_state->m_callback_mutex);
471 callback = timer.m_state->m_callback;
472 }
473 if (callback) {
474 detail::RunningTimerScope running_scope(timer.m_state.get());
475 try {
476 callback();
477 } catch (...) {
478 // TODO: integrate with logging once a logging facility is available.
479 }
480 }
481 finalize_timer(timer);
482 }
483 }
484
485 inline void TimerScheduler::finalize_timer(const detail::DueTimer& due_timer) {
486 auto state = due_timer.m_state;
487 if (!state) {
488 return;
489 }
490
491 std::unique_lock<std::mutex> lock(m_mutex);
492 state->m_is_running.store(false, std::memory_order_release);
493 if (!state->m_is_active.load(std::memory_order_relaxed)) {
494 return;
495 }
496
497 if (state->m_is_single_shot.load(std::memory_order_relaxed)) {
498 state->m_is_active.store(false, std::memory_order_relaxed);
499 state->m_generation.fetch_add(1, std::memory_order_relaxed);
500 return;
501 }
502
503 if (state->m_generation.load(std::memory_order_relaxed) != due_timer.m_generation) {
504 return;
505 }
506
507 const auto interval_ms = state->m_interval_ms.load(std::memory_order_relaxed);
508 const auto next_fire_time = due_timer.m_fire_time + std::chrono::milliseconds(interval_ms);
509 const auto next_generation = state->m_generation.fetch_add(1, std::memory_order_relaxed) + 1;
510 m_queue.push(detail::ScheduledTimer{next_fire_time, state->m_id, next_generation});
511 m_cv.notify_all();
512 }
513
514 // ---------------------------------------------------------------------
515 // Timer inline implementation
516 // ---------------------------------------------------------------------
517
518 inline Timer::Timer(TimerScheduler& scheduler)
519 : m_scheduler(scheduler), m_state(scheduler.create_timer_state()) {
520 if (m_state) {
521 m_state->m_has_external_owner.store(true, std::memory_order_relaxed);
522 }
523 }
524
525 inline Timer::~Timer() {
526 if (!m_state) {
527 return;
528 }
529
530 if (detail::current_timer_state() != m_state.get()) {
532 } else {
533 m_scheduler.stop_timer(m_state);
534 }
535
536 m_state->m_has_external_owner.store(false, std::memory_order_relaxed);
537 m_scheduler.destroy_timer_state(m_state);
538 }
539
540 template<class Rep, class Period>
541 void Timer::set_interval(std::chrono::duration<Rep, Period> interval) noexcept {
542 auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(interval).count();
543 if (milliseconds < 0) {
544 milliseconds = 0;
545 }
546 m_state->m_interval_ms.store(milliseconds, std::memory_order_relaxed);
547 }
548
549 inline std::chrono::milliseconds Timer::interval() const noexcept {
550 const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed);
551 return std::chrono::milliseconds(milliseconds);
552 }
553
554 inline void Timer::start() {
555 const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed);
556 const auto delay = TimerScheduler::clock::now() + std::chrono::milliseconds(milliseconds);
557 m_scheduler.start_timer(m_state, delay);
558 }
559
560 template<class Rep, class Period>
561 void Timer::start(std::chrono::duration<Rep, Period> interval) {
563 start();
564 }
565
566 inline void Timer::stop() {
567 m_scheduler.stop_timer(m_state);
568 }
569
570 inline void Timer::stop_and_wait() {
571 assert(detail::current_timer_state() != m_state.get()
572 && "stop_and_wait() must not be called from inside callback");
573 m_scheduler.stop_timer(m_state);
574 while (m_state->m_is_running.load(std::memory_order_acquire)) {
575 std::this_thread::yield();
576 }
577 }
578
579 inline void Timer::set_single_shot(bool is_single_shot) noexcept {
580 m_state->m_is_single_shot.store(is_single_shot, std::memory_order_relaxed);
581 }
582
583 inline bool Timer::is_single_shot() const noexcept {
584 return m_state->m_is_single_shot.load(std::memory_order_relaxed);
585 }
586
587 inline bool Timer::is_active() const noexcept {
588 return m_state->m_is_active.load(std::memory_order_relaxed);
589 }
590
591 inline bool Timer::is_running() const noexcept {
592 return m_state->m_is_running.load(std::memory_order_relaxed);
593 }
594
595 inline void Timer::set_callback(Callback callback) {
596 std::lock_guard<std::mutex> lock(m_state->m_callback_mutex);
597 m_state->m_callback = std::move(callback);
598 }
599
600 template<class Rep, class Period>
602 std::chrono::duration<Rep, Period> interval,
603 Callback callback) {
604 auto state = scheduler.create_timer_state();
605 if (!state) {
606 return;
607 }
608
609 auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(interval).count();
610 if (milliseconds < 0) {
611 milliseconds = 0;
612 }
613
614 state->m_is_single_shot.store(true, std::memory_order_relaxed);
615 state->m_interval_ms.store(milliseconds, std::memory_order_relaxed);
616
617 auto* scheduler_ptr = state->m_scheduler;
618
619 Callback user_callback_local = std::move(callback);
620
621 {
622 std::lock_guard<std::mutex> lock(state->m_callback_mutex);
623 state->m_callback = [state, scheduler_ptr, user_callback_local]() mutable {
624 if (user_callback_local) {
625 user_callback_local();
626 }
627
628 auto state_ptr = state;
629 if (!state_ptr) {
630 return;
631 }
632
633 {
634 std::lock_guard<std::mutex> callback_lock(state_ptr->m_callback_mutex);
635 state_ptr->m_callback = {};
636 }
637
638 if (scheduler_ptr) {
639 scheduler_ptr->destroy_timer_state(state_ptr);
640 }
641 };
642 }
643
644 const auto fire_time = TimerScheduler::clock::now() + std::chrono::milliseconds(milliseconds);
645 scheduler.start_timer(state, fire_time);
646 }
647
648} // namespace time_shield
649
650#endif // _TIME_SHIELD_TIMER_SCHEDULER_HPP_INCLUDED
Scheduler that manages timer execution.
void stop()
Requests the worker thread to stop and waits for it to exit.
TimerScheduler(const TimerScheduler &)=delete
std::unordered_map< std::size_t, std::weak_ptr< detail::TimerState > > m_timers
void run()
Starts a dedicated worker thread that processes timers.
std::size_t active_timer_count_for_testing()
Returns number of timer states that are still alive.
std::priority_queue< detail::ScheduledTimer, std::vector< detail::ScheduledTimer >, detail::ScheduledComparator > m_queue
TimerScheduler & operator=(TimerScheduler &&)=delete
void stop_timer(const timer_state_ptr &state)
void process()
Processes all timers that are ready to fire at the moment of the call.
TimerScheduler & operator=(const TimerScheduler &)=delete
void update()
Alias for process() for compatibility with update-based loops.
void finalize_timer(const detail::DueTimer &due_timer)
std::condition_variable m_cv
void collect_due_timers_locked(std::vector< detail::DueTimer > &due, clock::time_point now)
TimerScheduler(TimerScheduler &&)=delete
void execute_due_timers(std::vector< detail::DueTimer > &due)
timer_state_ptr create_timer_state()
void start_timer(const timer_state_ptr &state, clock::time_point when)
void destroy_timer_state(const timer_state_ptr &state)
Timer that mimics the behavior of Qt timers.
void start()
Starts the timer using the previously configured interval.
void set_single_shot(bool is_single_shot) noexcept
Sets whether the timer should fire only once.
timer_state_ptr m_state
Timer(TimerScheduler &scheduler)
detail::TimerCallback Callback
Timer & operator=(Timer &&)=delete
void set_callback(Callback callback)
Sets the callback that should be invoked when the timer fires.
bool is_active() const noexcept
Returns true if the timer is active.
Timer(const Timer &)=delete
static void single_shot(TimerScheduler &scheduler, std::chrono::duration< Rep, Period > interval, Callback callback)
Creates a single-shot timer that invokes the callback once.
void stop()
Stops the timer.
Timer & operator=(const Timer &)=delete
std::chrono::milliseconds interval() const noexcept
Returns the currently configured interval.
Timer(Timer &&)=delete
TimerScheduler & m_scheduler
bool is_single_shot() const noexcept
Returns true if the timer fires only once.
void stop_and_wait()
Stops the timer and waits until an active callback finishes.
void set_interval(std::chrono::duration< Rep, Period > interval) noexcept
Sets the interval used by the timer.
bool is_running() const noexcept
Returns true if the timer callback is being executed.
Configuration macros for the library.
ts_ms_t now() noexcept
Get the current UTC timestamp in milliseconds.
std::chrono::steady_clock TimerClock
TimerState *& current_timer_state()
std::function< void()> TimerCallback
Main namespace for the Time Shield library.
std::shared_ptr< detail::TimerState > timer_state_ptr
Helper structure that represents a timer ready to run.
std::shared_ptr< TimerState > m_state
TimerClock::time_point m_fire_time
DueTimer(TimerClock::time_point fire_time, std::uint64_t generation, std::shared_ptr< TimerState > state)
Comparator that orders timers by earliest fire time.
bool operator()(const ScheduledTimer &lhs, const ScheduledTimer &rhs) const
Data stored in the priority queue of scheduled timers.
ScheduledTimer(TimerClock::time_point fire_time, std::size_t timer_id, std::uint64_t generation)
Internal state shared between Timer and TimerScheduler.
std::atomic< std::int64_t > m_interval_ms
std::atomic< std::uint64_t > m_generation
std::atomic< bool > m_has_external_owner
std::atomic< bool > m_is_single_shot