Periodic Tasks
PeriodicTask submits a callback repeatedly to an executor.
It is useful for background work that must run again and again, such as metrics flushing, cache cleanup, health checks, polling, or internal maintenance.
The recommended include is:
#include <vix/threadpool/threadpool.hpp>Important idea
PeriodicTask owns a small scheduler thread that does not execute the user callback directly. Instead, it submits the callback to an executor:
PeriodicTask scheduler thread
→ waits for interval
→ posts callback to Executor
Executor
→ runs callbackThis keeps timing logic separate from task execution.
Basic usage
#include <atomic>
#include <chrono>
#include <iostream>
#include <thread>
#include <vix/threadpool.hpp>
int main()
{
vix::threadpool::ThreadPool pool(2);
std::atomic<int> ticks{0};
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{100};
config.run_immediately = true;
vix::threadpool::PeriodicTask task(
pool,
[&ticks]()
{
const int current = ticks.fetch_add(1, std::memory_order_relaxed) + 1;
std::cout << "tick: " << current << '\n';
},
config);
task.start();
std::this_thread::sleep_for(std::chrono::milliseconds{350});
task.stop();
task.join();
pool.wait_idle();
pool.shutdown();
return 0;
}Configuration
Periodic tasks are configured with PeriodicTaskConfig:
vix::threadpool::PeriodicTaskConfig config;interval
Controls how long the periodic scheduler waits between submissions. Default is 1000 ms.
config.interval = std::chrono::milliseconds{100};Intervals ≤ 0 are normalized to 1 ms. You can also create a config with:
auto config = vix::threadpool::PeriodicTaskConfig::every(std::chrono::milliseconds{250});run_immediately
By default the first callback is not submitted immediately (false). Set to true to submit on start:
config.run_immediately = false; // wait → submit → wait → submit ...
config.run_immediately = true; // submit → wait → submit → wait ...stop_on_post_failure
By default (true), periodic submission stops if the executor rejects a callback. Most applications should keep this default:
config.stop_on_post_failure = true; // safest default
config.stop_on_post_failure = false; // continue after failed poststask_options
Every submitted callback receives these task options:
config.task_options.set_priority(vix::threadpool::TaskPriority::low);Create and start a periodic task
The constructor does not start the task automatically:
vix::threadpool::PeriodicTask task(pool, []() { flush_metrics(); }, config);
task.start();start()
Returns true if the scheduler thread was started. Returns false if: no executor, empty callback, already running, or thread creation failed.
if (!task.start())
{
std::cout << "failed to start periodic task\n";
}Calling start() while already running returns false (idempotent while running).
Stop and join
task.stop(); // requests stop (non-blocking)
task.join(); // waits for scheduler thread to exitThe destructor calls stop() and join() automatically, but explicit lifecycle management is clearer in services and tests.
State checks
task.running(); // scheduler is active
task.joinable(); // scheduler thread can be joinedCounters
task.submitted_ticks(); // callbacks successfully posted to executor
task.failed_posts(); // callback submissions that failedA post fails when the executor is stopped, rejects the task, or is no longer valid.
Examples
Metrics flushing
#include <chrono>
#include <iostream>
#include <thread>
#include <vix/threadpool.hpp>
int main()
{
vix::threadpool::ThreadPool pool(2);
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{100};
config.run_immediately = true;
config.task_options.set_priority(vix::threadpool::TaskPriority::low);
vix::threadpool::PeriodicTask flushTask(
pool,
[]() { std::cout << "flush metrics\n"; },
config);
flushTask.start();
std::this_thread::sleep_for(std::chrono::milliseconds{350});
flushTask.stop();
flushTask.join();
pool.wait_idle();
std::cout << "submitted: " << flushTask.submitted_ticks() << '\n';
pool.shutdown();
return 0;
}Cache cleanup
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::seconds{5};
config.run_immediately = false;
config.task_options.set_priority(vix::threadpool::TaskPriority::low);
vix::threadpool::PeriodicTask cleanup(
pool, []() { cleanup_expired_cache_entries(); }, config);
cleanup.start();Health check
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::seconds{1};
config.run_immediately = true;
config.task_options.set_priority(vix::threadpool::TaskPriority::high);
vix::threadpool::PeriodicTask health(
pool, []() { refresh_health_snapshot(); }, config);
health.start();Using InlineExecutor
For deterministic tests, use InlineExecutor. Callbacks run directly on the periodic scheduler thread:
vix::threadpool::InlineExecutor executor;
std::atomic<int> ticks{0};
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{10};
config.run_immediately = true;
vix::threadpool::PeriodicTask task(
executor, [&ticks]() { ticks.fetch_add(1, std::memory_order_relaxed); }, config);
task.start();
std::this_thread::sleep_for(std::chrono::milliseconds{30});
task.stop();
task.join();Callback overlap
PeriodicTask does not wait for the previous callback to finish before submitting the next one. If the interval is short and the callback is slow, multiple callbacks can run at the same time.
Prevent overlapping callbacks
std::atomic<bool> running{false};
vix::threadpool::PeriodicTask task(
pool,
[&running]()
{
bool expected = false;
if (!running.compare_exchange_strong(expected, true))
{
return; // previous callback still running
}
try { do_work(); }
catch (...) { running.store(false); throw; }
running.store(false);
},
config);Combining with other features
Cancellation token
vix::threadpool::CancellationSource source;
auto token = source.token();
vix::threadpool::PeriodicTask task(
pool,
[token]()
{
if (token.cancelled()) { return; }
do_work();
},
config);
task.start();
source.request_cancel();
task.stop();
task.join();stop() controls periodic submission. The token controls callback logic.
Timeout on each submission
vix::threadpool::PeriodicTaskConfig config;
config.interval = std::chrono::milliseconds{100};
config.task_options.set_timeout(vix::threadpool::Timeout::milliseconds(50));Timeouts are observational — Vix does not forcibly kill running C++ code.
Shutdown order
Always stop periodic tasks before shutting down the pool:
// Good:
task.stop();
task.join();
pool.wait_idle();
pool.shutdown();
// Bad:
pool.shutdown();
task.stop();
task.join();stop() stops future submissions. Already-submitted callbacks may still be running. Use pool.wait_idle() to let them finish.
Common mistakes
Forgetting join
// Bad:
task.stop();
// missing task.join()
// Good:
task.stop();
task.join();Callback takes longer than interval
If the callback consistently takes longer than the interval, callbacks will overlap. Use a non-overlap guard if that is not acceptable.
Using tiny intervals in production
Very small intervals (e.g., 1 ms) may create too many submissions. Prefer realistic intervals:
metrics flush: 1s – 10s
cache cleanup: 5s – 60s
health refresh: 500ms – 5sWhen to use PeriodicTask
Good use cases: metrics flushing, cache cleanup, health checks, periodic polling, background maintenance, runtime housekeeping, retry ticks, queue draining checks.
Avoid when: you need precise real-time scheduling, callbacks must never overlap but you don't guard them, the interval is extremely small, the callback blocks forever, or the executor may be destroyed before the periodic task stops.
Simple mental model
PeriodicTask
owns scheduler thread
waits for interval
posts callback to Executor
tracks submitted ticks and failed posts
stop() stops future submissions
join() waits for scheduler thread
pool.wait_idle() waits for submitted callbacksThe normal pattern:
vix::threadpool::PeriodicTask task(pool, callback, config);
task.start();
// ... later ...
task.stop();
task.join();
pool.wait_idle();
pool.shutdown();