Skip to content

Commit 22e4771

Browse files
committed
fix(timer): harden scheduler concurrency
- guard callback access with a mutex and store intervals atomically to avoid data races\n- document worker vs manual processing semantics, add stop_and_wait(), and enforce process() assertions\n- adjust the timer scheduler test to cover stop_and_wait() on worker-driven timers
1 parent c5b712c commit 22e4771

2 files changed

Lines changed: 72 additions & 30 deletions

File tree

include/time_shield/TimerScheduler.hpp

Lines changed: 63 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
/// \brief Timer scheduler that provides Qt-like timer functionality.
88
///
99
/// TimerScheduler manages timers that can be processed either by a dedicated
10-
/// worker thread or manually via process/update calls.
10+
/// worker thread or manually via process/update calls. Timers are rescheduled
11+
/// using fixed-rate semantics, meaning the next activation time is based on the
12+
/// previously scheduled fire time. Cancelled timers are removed lazily from the
13+
/// internal queue, which can temporarily increase the queue size under frequent
14+
/// start/stop cycles.
1115

1216
#include "config.hpp"
1317

1418
#include <atomic>
19+
#include <cassert>
1520
#include <chrono>
1621
#include <condition_variable>
1722
#include <cstddef>
@@ -37,14 +42,15 @@ namespace time_shield {
3742

3843
/// \brief Internal state shared between Timer and TimerScheduler.
3944
struct TimerState {
40-
TimerScheduler* m_scheduler = nullptr;
41-
TimerCallback m_callback;
42-
std::chrono::milliseconds m_interval{0};
43-
std::atomic<bool> m_is_single_shot{false};
44-
std::atomic<bool> m_is_active{false};
45-
std::atomic<bool> m_is_running{false};
46-
std::size_t m_id{0};
47-
std::atomic<std::uint64_t> m_generation{0};
45+
TimerScheduler* m_scheduler = nullptr;
46+
std::mutex m_callback_mutex;
47+
TimerCallback m_callback;
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};
4854
};
4955

5056
/// \brief Data stored in the priority queue of scheduled timers.
@@ -100,7 +106,9 @@ namespace time_shield {
100106
/// \brief Starts a dedicated worker thread that processes timers.
101107
///
102108
/// This method is non-blocking. It spawns a background thread that
103-
/// waits for timers to fire and executes their callbacks.
109+
/// waits for timers to fire and executes their callbacks. While the
110+
/// worker thread is active, manual processing via process() or update()
111+
/// must not be used.
104112
void run();
105113

106114
/// \brief Requests the worker thread to stop and waits for it to exit.
@@ -109,6 +117,8 @@ namespace time_shield {
109117
/// \brief Processes all timers that are ready to fire at the moment of the call.
110118
///
111119
/// The method is non-blocking: it does not wait for future timers.
120+
/// It must not be called while the worker thread started by run() is
121+
/// active.
112122
void process();
113123

114124
/// \brief Alias for process() for compatibility with update-based loops.
@@ -151,6 +161,9 @@ namespace time_shield {
151161
Timer& operator=(Timer&&) = delete;
152162

153163
/// \brief Sets the interval used by the timer.
164+
///
165+
/// Negative durations are clamped to zero. An interval of zero means
166+
/// the timer is rescheduled immediately after firing.
154167
template<class Rep, class Period>
155168
void set_interval(std::chrono::duration<Rep, Period> interval) noexcept;
156169

@@ -165,8 +178,17 @@ namespace time_shield {
165178
void start(std::chrono::duration<Rep, Period> interval);
166179

167180
/// \brief Stops the timer.
181+
///
182+
/// The operation is non-blocking: the method does not wait for a
183+
/// running callback to finish. Use stop_and_wait() to synchronously
184+
/// wait for completion.
168185
void stop();
169186

187+
/// \brief Stops the timer and waits until an active callback finishes.
188+
///
189+
/// Must not be called from inside the timer callback itself.
190+
void stop_and_wait();
191+
170192
/// \brief Sets whether the timer should fire only once.
171193
void set_single_shot(bool is_single_shot) noexcept;
172194

@@ -183,6 +205,8 @@ namespace time_shield {
183205
void set_callback(Callback callback);
184206

185207
/// \brief Creates a single-shot timer that invokes the callback once.
208+
///
209+
/// The helper keeps the timer alive until the callback finishes.
186210
template<class Rep, class Period>
187211
static void single_shot(TimerScheduler& scheduler,
188212
std::chrono::duration<Rep, Period> interval,
@@ -235,14 +259,13 @@ namespace time_shield {
235259
lock.lock();
236260
m_is_worker_running = false;
237261
m_stop_requested = false;
238-
lock.unlock();
239-
m_thread = std::thread();
240262
}
241263

242264
inline void TimerScheduler::process() {
243265
std::vector<detail::DueTimer> due;
244266
{
245267
std::lock_guard<std::mutex> lock(m_mutex);
268+
assert(!m_is_worker_running && "process() must not be called while the worker thread is active");
246269
const auto now = clock::now();
247270
collect_due_timers_locked(due, now);
248271
}
@@ -358,15 +381,20 @@ namespace time_shield {
358381
continue;
359382
}
360383

361-
state->m_is_running.store(true, std::memory_order_relaxed);
384+
state->m_is_running.store(true, std::memory_order_release);
362385
due.push_back(detail::DueTimer{item.m_fire_time, item.m_generation, std::move(state)});
363386
}
364387
}
365388

366389
inline void TimerScheduler::execute_due_timers(std::vector<detail::DueTimer>& due) {
367390
for (auto& timer : due) {
368-
if (timer.m_state && timer.m_state->m_callback) {
369-
timer.m_state->m_callback();
391+
detail::TimerCallback callback;
392+
if (timer.m_state) {
393+
std::lock_guard<std::mutex> callback_lock(timer.m_state->m_callback_mutex);
394+
callback = timer.m_state->m_callback;
395+
}
396+
if (callback) {
397+
callback();
370398
}
371399
finalize_timer(timer);
372400
}
@@ -379,7 +407,7 @@ namespace time_shield {
379407
}
380408

381409
std::unique_lock<std::mutex> lock(m_mutex);
382-
state->m_is_running.store(false, std::memory_order_relaxed);
410+
state->m_is_running.store(false, std::memory_order_release);
383411
if (!state->m_is_active.load(std::memory_order_relaxed)) {
384412
return;
385413
}
@@ -394,7 +422,8 @@ namespace time_shield {
394422
return;
395423
}
396424

397-
const auto next_fire_time = due_timer.m_fire_time + state->m_interval;
425+
const auto interval_ms = state->m_interval_ms.load(std::memory_order_relaxed);
426+
const auto next_fire_time = due_timer.m_fire_time + std::chrono::milliseconds(interval_ms);
398427
const auto next_generation = state->m_generation.fetch_add(1, std::memory_order_relaxed) + 1;
399428
m_queue.push(detail::ScheduledTimer{next_fire_time, state->m_id, next_generation});
400429
m_cv.notify_all();
@@ -413,20 +442,21 @@ namespace time_shield {
413442

414443
template<class Rep, class Period>
415444
void Timer::set_interval(std::chrono::duration<Rep, Period> interval) noexcept {
416-
const auto converted = std::chrono::duration_cast<std::chrono::milliseconds>(interval);
417-
if (converted.count() < 0) {
418-
m_state->m_interval = std::chrono::milliseconds(0);
419-
} else {
420-
m_state->m_interval = converted;
445+
auto milliseconds = std::chrono::duration_cast<std::chrono::milliseconds>(interval).count();
446+
if (milliseconds < 0) {
447+
milliseconds = 0;
421448
}
449+
m_state->m_interval_ms.store(milliseconds, std::memory_order_relaxed);
422450
}
423451

424452
inline std::chrono::milliseconds Timer::interval() const noexcept {
425-
return m_state->m_interval;
453+
const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed);
454+
return std::chrono::milliseconds(milliseconds);
426455
}
427456

428457
inline void Timer::start() {
429-
const auto delay = TimerScheduler::clock::now() + m_state->m_interval;
458+
const auto milliseconds = m_state->m_interval_ms.load(std::memory_order_relaxed);
459+
const auto delay = TimerScheduler::clock::now() + std::chrono::milliseconds(milliseconds);
430460
m_scheduler.start_timer(m_state, delay);
431461
}
432462

@@ -440,6 +470,13 @@ namespace time_shield {
440470
m_scheduler.stop_timer(m_state);
441471
}
442472

473+
inline void Timer::stop_and_wait() {
474+
m_scheduler.stop_timer(m_state);
475+
while (m_state->m_is_running.load(std::memory_order_acquire)) {
476+
std::this_thread::yield();
477+
}
478+
}
479+
443480
inline void Timer::set_single_shot(bool is_single_shot) noexcept {
444481
m_state->m_is_single_shot.store(is_single_shot, std::memory_order_relaxed);
445482
}
@@ -457,6 +494,7 @@ namespace time_shield {
457494
}
458495

459496
inline void Timer::set_callback(Callback callback) {
497+
std::lock_guard<std::mutex> lock(m_state->m_callback_mutex);
460498
m_state->m_callback = std::move(callback);
461499
}
462500

@@ -466,13 +504,11 @@ namespace time_shield {
466504
Callback callback) {
467505
auto timer = std::shared_ptr<Timer>(new Timer(scheduler));
468506
timer->set_single_shot(true);
469-
std::shared_ptr<Callback> callback_holder(new Callback(std::move(callback)));
507+
auto callback_holder = std::make_shared<Callback>(std::move(callback));
470508
timer->set_callback([timer, callback_holder]() mutable {
471509
if (*callback_holder) {
472510
(*callback_holder)();
473511
}
474-
timer->stop();
475-
timer.reset();
476512
});
477513
timer->start(interval);
478514
}

tests/timer_scheduler_test.cpp

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,20 @@ int main() {
4343
}
4444
assert(repeating_counter.load() >= 3);
4545

46-
// Worker thread driven timer.
46+
// Worker thread driven timer with stop_and_wait().
4747
Timer worker_timer(scheduler);
4848
std::atomic<int> worker_counter{0};
4949
worker_timer.set_single_shot(true);
50-
worker_timer.set_callback([&worker_counter]() { worker_counter.fetch_add(1); });
50+
worker_timer.set_callback([&worker_counter]() {
51+
worker_counter.fetch_add(1);
52+
std::this_thread::sleep_for(std::chrono::milliseconds(20));
53+
});
5154
scheduler.run();
5255
worker_timer.start(std::chrono::milliseconds(10));
53-
std::this_thread::sleep_for(std::chrono::milliseconds(50));
56+
for (int i = 0; i < 50 && !worker_timer.is_running(); ++i) {
57+
std::this_thread::sleep_for(std::chrono::milliseconds(1));
58+
}
59+
worker_timer.stop_and_wait();
5460
scheduler.stop();
5561
assert(worker_counter.load() == 1);
5662

0 commit comments

Comments
 (0)