Background Task
This example shows how to run a small background worker with Vix Async.
It uses:
vix::async::core::io_context
vix::async::core::task<void>
timers
CPU pool
cooperative cancellation
signal handling
clean shutdownUse this example when you want to build:
periodic jobs
queue workers
cleanup jobs
email workers
cache refreshers
sync workers
metrics collectors
background maintenance tasksWhat this example builds
The program starts a background task that runs every second.
Each tick simulates a small job.
The program also listens for SIGINT.
Press:
Ctrl+Cand the worker stops cleanly.
The flow is:
create io_context
create cancellation source
start background worker
start signal watcher
run io_context
press Ctrl+C
request cancellation
worker exits
context stops
program exitsProject structure
Create:
background_task_demo/
└── background_task.cppCreate the project directory:
mkdir background_task_demo
cd background_task_demo
touch background_task.cppSource
Open:
background_task.cppAdd:
#include <chrono>
#include <csignal>
#include <exception>
#include <string>
#include <system_error>
#include <vix/async.hpp>
#include <vix/print.hpp>
using namespace std::chrono_literals;
static int compute_job_result(int tick)
{
return tick * 10;
}
static vix::async::core::task<void> run_background_worker(
vix::async::core::io_context &ctx,
vix::async::core::cancel_token token)
{
int tick = 0;
while (!token.is_cancelled())
{
++tick;
vix::print("worker tick =", tick);
try
{
const int result = co_await ctx.cpu_pool().submit([tick]()
{
return compute_job_result(tick);
}, token);
vix::print("job result =", result);
}
catch (const std::system_error &ex)
{
if (token.is_cancelled())
{
vix::print("worker cancellation observed");
break;
}
vix::eprint("worker system error:", ex.code().message());
}
catch (const std::exception &ex)
{
vix::eprint("worker error:", ex.what());
}
try
{
co_await ctx.timers().sleep_for(1s, token);
}
catch (const std::system_error &ex)
{
if (token.is_cancelled())
{
vix::print("worker timer cancelled");
break;
}
vix::eprint("timer error:", ex.code().message());
}
}
vix::print("background worker stopped");
ctx.stop();
co_return;
}
static vix::async::core::task<void> stop_on_signal(
vix::async::core::io_context &ctx,
vix::async::core::cancel_source &source)
{
ctx.signals().add(SIGINT);
ctx.signals().add(SIGTERM);
vix::print("background worker running");
vix::print("press Ctrl+C to stop");
const int sig = co_await ctx.signals().async_wait();
vix::print("shutdown signal received =", sig);
source.request_cancel();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
vix::async::core::cancel_source source;
auto worker = run_background_worker(ctx, source.token());
auto signal = stop_on_signal(ctx, source);
std::move(worker).start(ctx.get_scheduler());
std::move(signal).start(ctx.get_scheduler());
ctx.run();
ctx.shutdown();
vix::print("program stopped");
return 0;
}Run it
Run:
vix run background_task.cppExpected output shape:
background worker running
press Ctrl+C to stop
worker tick = 1
job result = 10
worker tick = 2
job result = 20
worker tick = 3
job result = 30Press:
Ctrl+CExpected shutdown output shape:
shutdown signal received = 2
worker timer cancelled
background worker stopped
program stoppedThe exact signal number can depend on the platform.
On many systems:
SIGINT = 2
SIGTERM = 15How it works
The program creates one async runtime:
vix::async::core::io_context ctx;The runtime owns the scheduler and async services.
The worker is started with:
auto worker = run_background_worker(ctx, source.token());
std::move(worker).start(ctx.get_scheduler());The signal watcher is started the same way:
auto signal = stop_on_signal(ctx, source);
std::move(signal).start(ctx.get_scheduler());Then the scheduler loop starts:
ctx.run();The program keeps running until something calls:
ctx.stop();Why the task receives a cancellation token
The worker receives:
vix::async::core::cancel_token tokenIt checks the token in the loop:
while (!token.is_cancelled())
{
// work
}The signal task owns the cancellation source:
vix::async::core::cancel_source source;When a signal is received, it requests cancellation:
source.request_cancel();The worker observes this through the token.
That is the important model:
cancel_source
requests cancellation
cancel_token
observes cancellationWhy the worker uses timers
The worker waits between ticks with:
co_await ctx.timers().sleep_for(1s, token);This is not the same as:
std::this_thread::sleep_for(...)A timer suspends the coroutine without blocking the scheduler thread.
That means the async runtime can keep processing other work while the worker waits.
Why the worker uses the CPU pool
The worker simulates a job with:
const int result = co_await ctx.cpu_pool().submit([tick]()
{
return compute_job_result(tick);
}, token);Use the CPU pool when the work is:
CPU-heavy
blocking
synchronous
slow enough to hurt the scheduler threadThe scheduler should stay responsive.
Avoid this inside a coroutine:
const int result = compute_job_result(tick);when the real function is expensive or blocking.
Prefer:
const int result = co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});Why signal handling is a separate task
The signal watcher is its own coroutine:
static vix::async::core::task<void> stop_on_signal(
vix::async::core::io_context &ctx,
vix::async::core::cancel_source &source)It registers signals:
ctx.signals().add(SIGINT);
ctx.signals().add(SIGTERM);Then waits asynchronously:
const int sig = co_await ctx.signals().async_wait();When a signal arrives, it requests cancellation:
source.request_cancel();This keeps shutdown logic clean.
The worker does the work.
The signal task only waits for shutdown.
Why the worker calls ctx.stop()
The signal task requests cancellation.
The worker exits.
Then the worker calls:
ctx.stop();This makes the runtime exit once the main background work is complete.
The final shutdown path is:
signal received
-> request cancellation
-> worker observes cancellation
-> worker exits loop
-> worker calls ctx.stop()
-> ctx.run() returns
-> ctx.shutdown()Add a second background task
You can start more than one worker.
Example:
auto worker_a = run_background_worker(ctx, source.token());
auto worker_b = run_background_worker(ctx, source.token());
std::move(worker_a).start(ctx.get_scheduler());
std::move(worker_b).start(ctx.get_scheduler());Both workers use the same cancellation token.
When the source requests cancellation, both workers can stop.
For real applications, make sure only one task owns the final ctx.stop() decision, or use a coordinator task.
Use a timer-only worker
For lightweight work, you may not need the CPU pool.
Example:
static vix::async::core::task<void> heartbeat(
vix::async::core::io_context &ctx,
vix::async::core::cancel_token token)
{
while (!token.is_cancelled())
{
vix::print("heartbeat");
try
{
co_await ctx.timers().sleep_for(1s, token);
}
catch (const std::system_error &)
{
break;
}
}
ctx.stop();
co_return;
}Use this shape for simple periodic logs, status checks, or metrics collection.
Use after for a one-shot background callback
For simple delayed callback work, use after.
#include <chrono>
#include <vix/async.hpp>
#include <vix/print.hpp>
using namespace std::chrono_literals;
int main()
{
vix::async::core::io_context ctx;
ctx.timers().after(500ms, [&ctx]()
{
vix::print("delayed callback");
ctx.stop();
});
ctx.run();
return 0;
}Use after when the work is callback-based.
Use sleep_for when the work is inside a coroutine.
Use post for immediate background work
For simple immediate work, use ctx.post.
#include <vix/async.hpp>
#include <vix/print.hpp>
int main()
{
vix::async::core::io_context ctx;
ctx.post([&ctx]()
{
vix::print("posted background callback");
ctx.stop();
});
ctx.run();
return 0;
}This is useful for simple callbacks that do not need co_await.
Background task in an HTTP application
For a standalone worker, io_context is enough.
For an HTTP application, a common shape is:
vix::App
handles HTTP
background worker
runs periodic work
shutdown signal
cancels background workerKeep the responsibilities separate:
HTTP routes
request and response logic
background task
scheduled work
cancellation source
shutdown coordinationA background task should not block HTTP request handling.
Use the async timer or the CPU pool when needed.
Common mistakes
Forgetting to run the context
Wrong:
vix::async::core::io_context ctx;
ctx.post([]()
{
vix::print("hello");
});Correct:
vix::async::core::io_context ctx;
ctx.post([&ctx]()
{
vix::print("hello");
ctx.stop();
});
ctx.run();Creating a task but not starting it
Wrong:
auto worker = run_background_worker(ctx, source.token());
ctx.run();Correct:
auto worker = run_background_worker(ctx, source.token());
std::move(worker).start(ctx.get_scheduler());
ctx.run();Tasks are lazy.
They must be started or awaited.
Using blocking sleep
Avoid:
std::this_thread::sleep_for(std::chrono::seconds(1));Prefer:
co_await ctx.timers().sleep_for(1s);The timer version does not block the scheduler thread.
Ignoring cancellation
Avoid:
while (true)
{
co_await ctx.timers().sleep_for(1s);
}Prefer:
while (!token.is_cancelled())
{
co_await ctx.timers().sleep_for(1s, token);
}Blocking inside the scheduler
Avoid expensive synchronous work directly inside the coroutine:
expensive_work();Prefer:
co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});Calling ctx.stop too early
If the signal task calls ctx.stop() immediately, the worker may not finish cleanup.
Prefer this flow:
signal task
requests cancellation
worker task
exits cleanly
stops contextBest practices
Use one io_context as the owner of async services.
Use task<void> for long-running coroutine workers.
Use sleep_for for periodic delays.
Use cancel_source and cancel_token for shutdown.
Use signals() to react to Ctrl+C and SIGTERM.
Use cpu_pool() for blocking or CPU-heavy work.
Keep the scheduler thread responsive.
Keep background loops cancellable.
Call ctx.shutdown() after ctx.run() returns.
Production notes
For production workers, add:
structured logs
error counters
retry policy
backoff
health status
metrics
job timeouts
clear shutdown policy
testsFor jobs that touch external systems, also add:
idempotency
deduplication
transaction boundaries
dead-letter handling
persistent queue stateA background task is simple when it only prints messages.
It becomes production-grade when it can survive retries, errors, cancellation, and process restarts.
Complete test flow
Run:
vix run background_task.cppWait for a few ticks:
worker tick = 1
job result = 10
worker tick = 2
job result = 20Press:
Ctrl+CExpected shape:
shutdown signal received = 2
worker timer cancelled
background worker stopped
program stoppedSummary
A background worker in Vix Async follows this shape:
create io_context
create cancel_source
start worker task
start signal task
run context
request cancellation on signal
worker stops
context stops
shutdown contextThe core code is:
vix::async::core::io_context ctx;
vix::async::core::cancel_source source;
auto worker = run_background_worker(ctx, source.token());
auto signal = stop_on_signal(ctx, source);
std::move(worker).start(ctx.get_scheduler());
std::move(signal).start(ctx.get_scheduler());
ctx.run();
ctx.shutdown();Use this pattern for small workers, periodic jobs, and background maintenance tasks in Vix applications.