From 4f34fef3b79626a6bd4bcafe1b8cf1884e8cead0 Mon Sep 17 00:00:00 2001 From: abin Date: Tue, 15 Sep 2026 23:56:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=BC=82=E6=AD=A5=20?= =?UTF-8?q?trigger()=EF=BC=8C=E4=BD=BF=E7=94=A8=20pending=20=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- include/simple_timer/simple_timer.h | 177 +++++++++++------ tests/test_timer.cpp | 292 ++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+), 57 deletions(-) diff --git a/include/simple_timer/simple_timer.h b/include/simple_timer/simple_timer.h index abbf61a..8194df8 100644 --- a/include/simple_timer/simple_timer.h +++ b/include/simple_timer/simple_timer.h @@ -29,10 +29,13 @@ #include #include #include +#include #include #include #include #include +#include +#include /** * @brief 使用 std::condition_variable 的 wait_until 方法 (也可以使用wait_for方法, 但是会累计误差) @@ -107,63 +110,31 @@ class SimpleTimer { template void start(Func &&f) { - stop(); // 确保没有其他线程在运行(替换旧任务) - state_ = State::Running; // 设置状态为运行中 - auto task = std::move(std::forward(f)); // 完美转发后再 move, 提高效率 - // 使用 std::thread 创建一个新的线程来执行定时器任务 - thread_ = std::thread([this, task]() mutable { - std::unique_lock lock(mutex_); - auto next_time = clock::now() + interval_; - while (true) - { - if (state_ == State::Stopped) - { - break; - } - - while (state_ == State::Paused) - { - cv_.wait(lock, [this]() { return state_ != State::Paused; }); - next_time = clock::now() + interval_; // 重新计算下一次触发时间 - } - - if (cv_.wait_until(lock, next_time, [this]() { return state_ != State::Running || interval_changed_; })) - { - if (interval_changed_) // interval_修改后立即使用新间隔 - { - next_time = clock::now() + interval_; - interval_changed_ = false; - } - continue; // 若状态不是 Running, 继续循环判断; 若是 interval_ 被修改, 则更新 next_time 并立即跳过等待 - } - - lock.unlock(); - // Timer 内部处理异常, 执行task遇到异常后直接停止timer - try - { - task(); // 执行任务 - } - catch (const std::exception &e) - { - state_ = State::Stopped; // 出现异常时停止定时器 (不能调用stop()会死锁) - std::fprintf(stderr, "\n\033[1;31m[SimpleTimer] Exception: %s\033[0m\n\n", e.what()); - } - catch (...) - { - state_ = State::Stopped; // 出现异常时停止定时器 - std::fprintf(stderr, "\n\033[1;31m[SimpleTimer] Unknown exception occurred.\033[0m\n\n"); - } - lock.lock(); - - if (one_shot_) - { - state_ = State::Stopped; - break; - } + stop(); // 确保没有其他线程在运行(替换旧任务) + typedef typename std::decay::type Task; + Task task(std::forward(f)); + { + std::lock_guard lock(mutex_); + pending_triggers_ = 0; + state_ = State::Running; + } + thread_ = std::thread(&SimpleTimer::run, this, std::move(task)); + } - next_time += interval_; // 精确推进时间点, 避免偏差 + /// @brief Requests immediate execution of the current timer task + /// @note The task is queued and executed asynchronously by the timer thread. + /// @note Requests are ignored while the timer is paused or stopped. + void trigger() + { + { + std::lock_guard lock(mutex_); + if (state_ != State::Running) + { + return; } - }); + ++pending_triggers_; + } + cv_.notify_all(); } /// @brief Restarts the timer @@ -180,7 +151,11 @@ class SimpleTimer { /// @note This method may block until the running task completes. void stop() { - state_ = State::Stopped; + { + std::lock_guard lock(mutex_); + state_ = State::Stopped; + pending_triggers_ = 0; + } cv_.notify_all(); // 唤醒等待的线程 if (thread_.joinable() && thread_.get_id() != std::this_thread::get_id()) { @@ -260,8 +235,96 @@ class SimpleTimer { } private: + template + void run(Task task) + { + std::unique_lock lock(mutex_); + auto next_time = clock::now() + interval_; + while (true) + { + if (state_ == State::Stopped) + { + break; + } + + while (state_ == State::Paused) + { + cv_.wait(lock, [this]() { return state_ != State::Paused; }); + next_time = clock::now() + interval_; + } + + if (state_ == State::Stopped) + { + break; + } + + bool scheduled_trigger = false; + if (pending_triggers_ == 0) + { + scheduled_trigger = !cv_.wait_until(lock, next_time, [this]() { + return state_ != State::Running || interval_changed_ || pending_triggers_ != 0; + }); + } + + if (state_ == State::Stopped) + { + break; + } + if (state_ == State::Paused) + { + continue; + } + + if (interval_changed_) + { + next_time = clock::now() + interval_; + interval_changed_ = false; + } + + const bool manual_trigger = pending_triggers_ != 0; + if (manual_trigger) + { + --pending_triggers_; + } + else if (!scheduled_trigger) + { + continue; + } + + lock.unlock(); + try + { + task(); + } + catch (const std::exception &e) + { + state_ = State::Stopped; + std::fprintf(stderr, "\n\033[1;31m[SimpleTimer] Exception: %s\033[0m\n\n", e.what()); + } + catch (...) + { + state_ = State::Stopped; + std::fprintf(stderr, "\n\033[1;31m[SimpleTimer] Unknown exception occurred.\033[0m\n\n"); + } + lock.lock(); + + if (one_shot_ || state_ == State::Stopped) + { + state_ = State::Stopped; + pending_triggers_ = 0; + break; + } + + if (!manual_trigger) + { + next_time += interval_; // 精确推进时间点, 避免偏差 + } + } + } + // 定时器间隔, 默认10秒 clock::duration interval_{std::chrono::seconds(10)}; + std::size_t pending_triggers_{0}; // 待执行的手动触发请求数 bool interval_changed_{false}; // 时间间隔是否被修改过 bool one_shot_{false}; // 是否只触发一次 std::atomic state_; // 定时器状态 @@ -270,4 +333,4 @@ class SimpleTimer { std::condition_variable cv_; // 条件变量, 用于暂停和恢复 }; -#endif // SIMPLE_TIMER_H \ No newline at end of file +#endif // SIMPLE_TIMER_H diff --git a/tests/test_timer.cpp b/tests/test_timer.cpp index d66b697..8ac17f5 100644 --- a/tests/test_timer.cpp +++ b/tests/test_timer.cpp @@ -4,10 +4,45 @@ #include #include #include +#include +#include +#include #include +#include using namespace std::chrono; +namespace +{ +template +bool wait_for_condition(Predicate predicate, milliseconds timeout = milliseconds(1000)) +{ + const auto deadline = steady_clock::now() + timeout; + while (!predicate() && steady_clock::now() < deadline) + { + std::this_thread::sleep_for(milliseconds(1)); + } + return predicate(); +} + +struct MoveOnlyTask +{ + MoveOnlyTask(std::unique_ptr value, std::atomic &result) : value_(std::move(value)), result_(result) {} + MoveOnlyTask(MoveOnlyTask &&) = default; + MoveOnlyTask &operator=(MoveOnlyTask &&) = delete; + MoveOnlyTask(const MoveOnlyTask &) = delete; + MoveOnlyTask &operator=(const MoveOnlyTask &) = delete; + + void operator()() + { + result_ += *value_; + } + + std::unique_ptr value_; + std::atomic &result_; +}; +} // namespace + TEST_CASE("SimpleTimer triggers task at interval", "[SimpleTimer]") { std::atomic counter(0); @@ -21,6 +56,263 @@ TEST_CASE("SimpleTimer triggers task at interval", "[SimpleTimer]") REQUIRE(counter <= 4); // 容许调度误差 } +TEST_CASE("Manual trigger executes a running timer before its interval", "[SimpleTimer][trigger]") +{ + std::atomic counter{0}; + SimpleTimer timer(seconds(5)); + timer.start([&]() { counter++; }); + + timer.trigger(); + + const auto deadline = steady_clock::now() + milliseconds(500); + while (counter.load() == 0 && steady_clock::now() < deadline) + { + std::this_thread::sleep_for(milliseconds(1)); + } + + timer.stop(); + REQUIRE(counter == 1); +} + +TEST_CASE("Manual trigger is ignored unless the timer is running", "[SimpleTimer][trigger]") +{ + std::atomic counter{0}; + SimpleTimer timer(seconds(5)); + + timer.trigger(); + REQUIRE_FALSE(wait_for_condition([&]() { return counter.load() != 0; }, milliseconds(30))); + + timer.start([&]() { counter++; }); + timer.pause(); + timer.trigger(); + REQUIRE_FALSE(wait_for_condition([&]() { return counter.load() != 0; }, milliseconds(30))); + + timer.resume(); + timer.trigger(); + REQUIRE(wait_for_condition([&]() { return counter.load() == 1; })); + + timer.stop(); + timer.trigger(); + REQUIRE_FALSE(wait_for_condition([&]() { return counter.load() != 1; }, milliseconds(30))); +} + +TEST_CASE("Manual trigger runs the callback on the timer thread", "[SimpleTimer][trigger]") +{ + const std::thread::id caller_id = std::this_thread::get_id(); + std::thread::id callback_id; + std::mutex callback_id_mutex; + std::atomic called{false}; + SimpleTimer timer(seconds(5)); + timer.start([&]() { + { + std::lock_guard lock(callback_id_mutex); + callback_id = std::this_thread::get_id(); + } + called = true; + }); + + timer.trigger(); + REQUIRE(wait_for_condition([&]() { return called.load(); })); + timer.stop(); + + std::lock_guard lock(callback_id_mutex); + REQUIRE(callback_id != caller_id); +} + +TEST_CASE("Manual trigger preserves every sequential and concurrent request", "[SimpleTimer][trigger]") +{ + std::atomic counter{0}; + SimpleTimer timer(seconds(5)); + timer.start([&]() { counter++; }); + + for (int i = 0; i < 5; ++i) + { + timer.trigger(); + } + + std::vector callers; + for (int thread_index = 0; thread_index < 4; ++thread_index) + { + callers.emplace_back([&]() { + for (int i = 0; i < 25; ++i) + { + timer.trigger(); + } + }); + } + for (std::size_t i = 0; i < callers.size(); ++i) + { + callers[i].join(); + } + + REQUIRE(wait_for_condition([&]() { return counter.load() == 105; }, milliseconds(2000))); + timer.stop(); + REQUIRE(counter == 105); +} + +TEST_CASE("Manual triggers queue while a callback is active and never overlap", "[SimpleTimer][trigger]") +{ + std::atomic calls{0}; + std::atomic active{0}; + std::atomic maximum_active{0}; + std::atomic release_first{false}; + SimpleTimer timer(seconds(5)); + timer.start([&]() { + const int now_active = ++active; + int observed = maximum_active.load(); + while (observed < now_active && !maximum_active.compare_exchange_weak(observed, now_active)) + { + } + + const int call_number = ++calls; + if (call_number == 1) + { + while (!release_first.load()) + { + std::this_thread::yield(); + } + } + --active; + }); + + timer.trigger(); + const bool first_call_started = wait_for_condition([&]() { return calls.load() == 1; }); + if (!first_call_started) + { + release_first = true; + timer.stop(); + } + REQUIRE(first_call_started); + timer.trigger(); + timer.trigger(); + timer.trigger(); + release_first = true; + + REQUIRE(wait_for_condition([&]() { return calls.load() == 4; })); + timer.stop(); + REQUIRE(maximum_active == 1); +} + +TEST_CASE("Manual trigger does not reset the scheduled deadline", "[SimpleTimer][trigger]") +{ + std::atomic counter{0}; + std::mutex times_mutex; + std::vector callback_times; + SimpleTimer timer(milliseconds(2000)); + timer.start([&]() { + { + std::lock_guard lock(times_mutex); + callback_times.push_back(steady_clock::now()); + } + counter++; + }); + + timer.trigger(); + REQUIRE(wait_for_condition([&]() { return counter.load() == 1; }, milliseconds(500))); + const auto worker_ready_at = steady_clock::now(); + + std::this_thread::sleep_until(worker_ready_at + milliseconds(500)); + REQUIRE(counter == 1); + const auto second_triggered_at = steady_clock::now(); + timer.trigger(); + REQUIRE(wait_for_condition([&]() { return counter.load() == 2; }, milliseconds(500))); + + const bool scheduled_call_arrived = wait_for_condition([&]() { return counter.load() >= 3; }, milliseconds(2000)); + timer.stop(); + REQUIRE(scheduled_call_arrived); + + std::lock_guard lock(times_mutex); + REQUIRE(callback_times.size() >= 3); + REQUIRE(callback_times[2] - second_triggered_at < milliseconds(1800)); +} + +TEST_CASE("Stop discards pending manual triggers and restart begins with an empty queue", "[SimpleTimer][trigger]") +{ + std::atomic calls{0}; + std::atomic release_first{false}; + SimpleTimer timer(seconds(5)); + timer.start([&]() { + const int call_number = ++calls; + if (call_number == 1) + { + while (!release_first.load()) + { + std::this_thread::yield(); + } + } + }); + + timer.trigger(); + const bool first_call_started = wait_for_condition([&]() { return calls.load() == 1; }); + if (!first_call_started) + { + release_first = true; + timer.stop(); + } + REQUIRE(first_call_started); + for (int i = 0; i < 5; ++i) + { + timer.trigger(); + } + + std::thread stopper([&]() { timer.stop(); }); + const bool stop_started = wait_for_condition([&]() { return timer.is_stopped(); }); + release_first = true; + stopper.join(); + REQUIRE(stop_started); + REQUIRE(calls == 1); + + timer.restart([&]() { calls++; }); + REQUIRE_FALSE(wait_for_condition([&]() { return calls.load() != 1; }, milliseconds(30))); + timer.trigger(); + REQUIRE(wait_for_condition([&]() { return calls.load() == 2; })); + timer.stop(); +} + +TEST_CASE("Manual trigger consumes a one-shot timer", "[SimpleTimer][trigger]") +{ + std::atomic counter{0}; + SimpleTimer timer(seconds(5), true); + timer.start([&]() { counter++; }); + + timer.trigger(); + timer.trigger(); + timer.trigger(); + + REQUIRE(wait_for_condition([&]() { return timer.is_stopped(); })); + timer.trigger(); + std::this_thread::sleep_for(milliseconds(30)); + timer.stop(); + REQUIRE(counter == 1); +} + +TEST_CASE("Exception from a manually triggered callback stops the timer", "[SimpleTimer][trigger]") +{ + SimpleTimer timer(seconds(5)); + timer.start([]() { throw std::runtime_error("manual trigger failure"); }); + + timer.trigger(); + + REQUIRE(wait_for_condition([&]() { return timer.is_stopped(); })); + timer.stop(); +} + +TEST_CASE("Move-only tasks support manual and scheduled execution", "[SimpleTimer][trigger]") +{ + std::atomic manual_result{0}; + SimpleTimer manual_timer(seconds(5)); + manual_timer.start(MoveOnlyTask(std::unique_ptr(new int(7)), manual_result)); + manual_timer.trigger(); + REQUIRE(wait_for_condition([&]() { return manual_result.load() == 7; })); + manual_timer.stop(); + + std::atomic scheduled_result{0}; + SimpleTimer scheduled_timer(milliseconds(20), true); + scheduled_timer.start(MoveOnlyTask(std::unique_ptr(new int(9)), scheduled_result)); + REQUIRE(wait_for_condition([&]() { return scheduled_result.load() == 9; })); + scheduled_timer.stop(); +} + TEST_CASE("Stop prevents further execution", "[SimpleTimer]") { std::atomic counter{0};