Vix.cpp v2.6.0 is here Read the blog
Skip to content

Background Task

This example shows how to run a small background worker with Vix Async.

It uses:

txt
vix::async::core::io_context
vix::async::core::task<void>
timers
CPU pool
cooperative cancellation
signal handling
clean shutdown

Use this example when you want to build:

txt
periodic jobs
queue workers
cleanup jobs
email workers
cache refreshers
sync workers
metrics collectors
background maintenance tasks

What 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:

txt
Ctrl+C

and the worker stops cleanly.

The flow is:

txt
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 exits

Project structure

Create:

txt
background_task_demo/
└── background_task.cpp

Create the project directory:

bash
mkdir background_task_demo
cd background_task_demo
touch background_task.cpp

Source

Open:

txt
background_task.cpp

Add:

cpp
#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:

bash
vix run background_task.cpp

Expected output shape:

txt
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 = 30

Press:

txt
Ctrl+C

Expected shutdown output shape:

txt
shutdown signal received = 2
worker timer cancelled
background worker stopped
program stopped

The exact signal number can depend on the platform.

On many systems:

txt
SIGINT = 2
SIGTERM = 15

How it works

The program creates one async runtime:

cpp
vix::async::core::io_context ctx;

The runtime owns the scheduler and async services.

The worker is started with:

cpp
auto worker = run_background_worker(ctx, source.token());

std::move(worker).start(ctx.get_scheduler());

The signal watcher is started the same way:

cpp
auto signal = stop_on_signal(ctx, source);

std::move(signal).start(ctx.get_scheduler());

Then the scheduler loop starts:

cpp
ctx.run();

The program keeps running until something calls:

cpp
ctx.stop();

Why the task receives a cancellation token

The worker receives:

cpp
vix::async::core::cancel_token token

It checks the token in the loop:

cpp
while (!token.is_cancelled())
{
  // work
}

The signal task owns the cancellation source:

cpp
vix::async::core::cancel_source source;

When a signal is received, it requests cancellation:

cpp
source.request_cancel();

The worker observes this through the token.

That is the important model:

txt
cancel_source
  requests cancellation

cancel_token
  observes cancellation

Why the worker uses timers

The worker waits between ticks with:

cpp
co_await ctx.timers().sleep_for(1s, token);

This is not the same as:

cpp
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:

cpp
const int result = co_await ctx.cpu_pool().submit([tick]()
{
  return compute_job_result(tick);
}, token);

Use the CPU pool when the work is:

txt
CPU-heavy
blocking
synchronous
slow enough to hurt the scheduler thread

The scheduler should stay responsive.

Avoid this inside a coroutine:

cpp
const int result = compute_job_result(tick);

when the real function is expensive or blocking.

Prefer:

cpp
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:

cpp
static vix::async::core::task<void> stop_on_signal(
  vix::async::core::io_context &ctx,
  vix::async::core::cancel_source &source)

It registers signals:

cpp
ctx.signals().add(SIGINT);
ctx.signals().add(SIGTERM);

Then waits asynchronously:

cpp
const int sig = co_await ctx.signals().async_wait();

When a signal arrives, it requests cancellation:

cpp
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:

cpp
ctx.stop();

This makes the runtime exit once the main background work is complete.

The final shutdown path is:

txt
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:

cpp
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:

cpp
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.

cpp
#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.

cpp
#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:

txt
vix::App
  handles HTTP

background worker
  runs periodic work

shutdown signal
  cancels background worker

Keep the responsibilities separate:

txt
HTTP routes
  request and response logic

background task
  scheduled work

cancellation source
  shutdown coordination

A 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:

cpp
vix::async::core::io_context ctx;

ctx.post([]()
{
  vix::print("hello");
});

Correct:

cpp
vix::async::core::io_context ctx;

ctx.post([&ctx]()
{
  vix::print("hello");
  ctx.stop();
});

ctx.run();

Creating a task but not starting it

Wrong:

cpp
auto worker = run_background_worker(ctx, source.token());

ctx.run();

Correct:

cpp
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:

cpp
std::this_thread::sleep_for(std::chrono::seconds(1));

Prefer:

cpp
co_await ctx.timers().sleep_for(1s);

The timer version does not block the scheduler thread.

Ignoring cancellation

Avoid:

cpp
while (true)
{
  co_await ctx.timers().sleep_for(1s);
}

Prefer:

cpp
while (!token.is_cancelled())
{
  co_await ctx.timers().sleep_for(1s, token);
}

Blocking inside the scheduler

Avoid expensive synchronous work directly inside the coroutine:

cpp
expensive_work();

Prefer:

cpp
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:

txt
signal task
  requests cancellation

worker task
  exits cleanly
  stops context

Best 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:

txt
structured logs
error counters
retry policy
backoff
health status
metrics
job timeouts
clear shutdown policy
tests

For jobs that touch external systems, also add:

txt
idempotency
deduplication
transaction boundaries
dead-letter handling
persistent queue state

A 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:

bash
vix run background_task.cpp

Wait for a few ticks:

txt
worker tick = 1
job result = 10
worker tick = 2
job result = 20

Press:

txt
Ctrl+C

Expected shape:

txt
shutdown signal received = 2
worker timer cancelled
background worker stopped
program stopped

Summary

A background worker in Vix Async follows this shape:

txt
create io_context
create cancel_source
start worker task
start signal task
run context
request cancellation on signal
worker stops
context stops
shutdown context

The core code is:

cpp
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.

Released under the MIT License.