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

Async App

This example shows how to use Vix Async inside a Vix application.

Use this when you want an HTTP app that also runs background async work.

The goal is to keep the model clear:

txt
vix::App
  handles HTTP requests

vix::async::core::io_context
  runs async background work

shared application state
  connects background work to HTTP routes

cancellation
  stops the worker cleanly
1
2
3
4
5
6
7
8
9
10
11

This is different from a standalone background worker.

background-task.md shows an async worker by itself.

This page shows async work running next to an HTTP application.

What this example builds

The app exposes:

txt
GET /api/health
GET /api/status
POST /api/refresh
1
2
3

At the same time, an async worker runs in the background every second.

The worker updates shared state:

txt
tick count
last job result
running flag
1
2
3

The HTTP routes can read that state.

Project structure

Create:

txt
async_app_demo/
└── async_app.cpp
1
2

Create the file:

bash
mkdir async_app_demo
cd async_app_demo
touch async_app.cpp
1
2
3

Source

Open:

txt
async_app.cpp
1

Add:

cpp
#include <atomic>
#include <chrono>
#include <csignal>
#include <exception>
#include <memory>
#include <mutex>
#include <string>
#include <thread>

#include <vix.hpp>
#include <vix/async.hpp>
#include <vix/middleware.hpp>
#include <vix/print.hpp>

using namespace std::chrono_literals;

struct AppState
{
  std::mutex mutex{};
  int tick_count{0};
  int last_result{0};
  bool worker_running{false};
};

static int compute_background_result(int tick)
{
  return tick * 10;
}

static void set_worker_running(
  const std::shared_ptr<AppState> &state,
  bool running)
{
  std::lock_guard<std::mutex> lock(state->mutex);
  state->worker_running = running;
}

static void update_background_state(
  const std::shared_ptr<AppState> &state,
  int tick,
  int result)
{
  std::lock_guard<std::mutex> lock(state->mutex);

  state->tick_count = tick;
  state->last_result = result;
  state->worker_running = true;
}

static vix::async::core::task<void> background_refresh_worker(
  vix::async::core::io_context &ctx,
  std::shared_ptr<AppState> state,
  vix::async::core::cancel_token token)
{
  int tick = 0;

  set_worker_running(state, true);

  while (!token.is_cancelled())
  {
    ++tick;

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

      update_background_state(state, tick, result);

      vix::print("background refresh tick =", tick);
    }
    catch (const std::exception &ex)
    {
      if (token.is_cancelled())
        break;

      vix::eprint("background refresh error:", ex.what());
    }

    try
    {
      co_await ctx.timers().sleep_for(1s, token);
    }
    catch (const std::exception &)
    {
      if (token.is_cancelled())
        break;
    }
  }

  set_worker_running(state, false);

  vix::print("background refresh worker stopped");

  co_return;
}

static vix::async::core::task<void> stop_async_on_signal(
  vix::async::core::io_context &ctx,
  vix::async::core::cancel_source &source)
{
  ctx.signals().add(SIGINT);
  ctx.signals().add(SIGTERM);

  const int signal = co_await ctx.signals().async_wait();

  vix::print("shutdown signal received =", signal);

  source.request_cancel();

  co_await ctx.timers().sleep_for(100ms);

  ctx.stop();

  co_return;
}

static void install_middleware(vix::App &app)
{
  app.use("/api", vix::middleware::app::recovery_dev());
  app.use("/api", vix::middleware::app::request_id_dev());
  app.use("/api", vix::middleware::app::timing_dev());
  app.use("/api", vix::middleware::app::security_headers_dev());
  app.use("/api", vix::middleware::app::rate_limit_dev());
}

static void register_routes(
  vix::App &app,
  const std::shared_ptr<AppState> &state,
  vix::async::core::io_context &async_ctx)
{
  app.get("/", [](vix::Request &, vix::Response &res)
  {
    res.text("Async App example. Try /api/status.");
  });

  app.get("/api/health", [](vix::Request &, vix::Response &res)
  {
    res.json({
      "ok", true,
      "service", "async-app"
    });
  });

  app.get("/api/status", [state](vix::Request &, vix::Response &res)
  {
    int tick_count = 0;
    int last_result = 0;
    bool worker_running = false;

    {
      std::lock_guard<std::mutex> lock(state->mutex);

      tick_count = state->tick_count;
      last_result = state->last_result;
      worker_running = state->worker_running;
    }

    res.json({
      "ok", true,
      "worker_running", worker_running,
      "tick_count", tick_count,
      "last_result", last_result
    });
  });

  app.post("/api/refresh", [state, &async_ctx](vix::Request &, vix::Response &res)
  {
    async_ctx.post([state]()
    {
      std::lock_guard<std::mutex> lock(state->mutex);

      state->tick_count += 1;
      state->last_result = state->tick_count * 100;
      state->worker_running = true;
    });

    res.status(202).json({
      "ok", true,
      "queued", true
    });
  });
}

static void run_async_runtime(
  vix::async::core::io_context &ctx)
{
  ctx.run();
  ctx.shutdown();
}

int main()
{
  auto state = std::make_shared<AppState>();

  vix::async::core::io_context async_ctx;
  vix::async::core::cancel_source cancel_source;

  auto worker =
    background_refresh_worker(
      async_ctx,
      state,
      cancel_source.token()
    );

  auto signal =
    stop_async_on_signal(
      async_ctx,
      cancel_source
    );

  std::move(worker).start(async_ctx.get_scheduler());
  std::move(signal).start(async_ctx.get_scheduler());

  std::thread async_thread([&async_ctx]()
  {
    run_async_runtime(async_ctx);
  });

  vix::App app;

  install_middleware(app);
  register_routes(app, state, async_ctx);

  app.run(8080);

  cancel_source.request_cancel();
  async_ctx.stop();

  if (async_thread.joinable())
    async_thread.join();

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236

Run it

Run:

bash
vix run async_app.cpp
1

The HTTP server listens on:

txt
http://127.0.0.1:8080
1

The async worker starts in a separate runtime thread.

Test health

bash
curl -i http://127.0.0.1:8080/api/health
1

Expected body:

json
{
  "ok": true,
  "service": "async-app"
}
1
2
3
4

Test background status

bash
curl -i http://127.0.0.1:8080/api/status
1

Expected body shape:

json
{
  "ok": true,
  "worker_running": true,
  "tick_count": 3,
  "last_result": 30
}
1
2
3
4
5
6

Call it again after a few seconds:

bash
curl -i http://127.0.0.1:8080/api/status
1

You should see tick_count and last_result changing.

Trigger one background update from HTTP

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/refresh
1
2

Expected status:

txt
202 Accepted
1

Expected body:

json
{
  "ok": true,
  "queued": true
}
1
2
3
4

This route does not run the work directly inside the HTTP handler.

It schedules work into the async runtime:

cpp
async_ctx.post([state]()
{
  std::lock_guard<std::mutex> lock(state->mutex);

  state->tick_count += 1;
  state->last_result = state->tick_count * 100;
  state->worker_running = true;
});
1
2
3
4
5
6
7
8

That keeps the handler small.

Why use a separate async runtime thread

vix::App::run(...) blocks while the HTTP server is running.

io_context::run() also blocks while async work is running.

So this example runs the async context in another thread:

cpp
std::thread async_thread([&async_ctx]()
{
  run_async_runtime(async_ctx);
});
1
2
3
4

The HTTP app runs on the main thread:

cpp
app.run(8080);
1

The model is:

txt
main thread
  HTTP server

async thread
  background worker
  timers
  CPU pool tasks
  signal watcher
1
2
3
4
5
6
7
8

This keeps the example simple and explicit.

Shared state

The HTTP routes and async worker share:

cpp
std::shared_ptr<AppState>
1

The state is protected by a mutex:

cpp
struct AppState
{
  std::mutex mutex{};
  int tick_count{0};
  int last_result{0};
  bool worker_running{false};
};
1
2
3
4
5
6
7

The worker writes the state:

cpp
update_background_state(state, tick, result);
1

The HTTP route reads the state:

cpp
std::lock_guard<std::mutex> lock(state->mutex);

tick_count = state->tick_count;
last_result = state->last_result;
worker_running = state->worker_running;
1
2
3
4
5

Use this pattern for small shared status.

For larger systems, prefer a service class.

Better structure for bigger apps

For bigger applications, do not keep everything in one file.

Use a service:

txt
AsyncRuntime
  owns io_context
  owns cancel_source
  owns thread
  starts workers
  stops workers

AppState
  owns shared state

RouteRegistry
  registers HTTP routes

AppBootstrap
  wires everything together
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

The single-file example is useful for learning.

A production app should move this logic into classes.

Why the worker uses timers

The worker waits with:

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

This is better than:

cpp
std::this_thread::sleep_for(1s);
1

because the coroutine sleep does not block the scheduler thread.

The async runtime can still process posted callbacks and other async work.

Why the worker uses the CPU pool

The worker offloads work to:

cpp
ctx.cpu_pool().submit(...)
1

Example:

cpp
const int result = co_await ctx.cpu_pool().submit([tick]()
{
  return compute_background_result(tick);
}, token);
1
2
3
4

Use the CPU pool when the work may be:

txt
CPU-heavy
blocking
synchronous
slow enough to affect the scheduler
1
2
3
4

For tiny work, direct computation is fine.

For real backend work, avoid blocking the scheduler thread.

Cancellation

The app creates:

cpp
vix::async::core::cancel_source cancel_source;
1

The worker receives:

cpp
cancel_source.token()
1

When the app shuts down, cancellation is requested:

cpp
cancel_source.request_cancel();
1

The worker checks:

cpp
while (!token.is_cancelled())
1

and cancellable waits use the token:

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

The rule is:

txt
cancel_source requests shutdown
cancel_token lets tasks observe shutdown
1
2

Signal handling

The async runtime listens for:

cpp
SIGINT
SIGTERM
1
2

with:

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

Then waits:

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

When a signal is received:

cpp
source.request_cancel();
1

This is useful for terminal shutdown:

txt
Ctrl+C
1

and process managers:

txt
SIGTERM
1

Shutdown flow

The shutdown flow is:

txt
Ctrl+C
  -> signal task wakes up
  -> cancellation requested
  -> worker observes cancellation
  -> async context stops
  -> async thread joins
  -> process exits
1
2
3
4
5
6
7

The code also handles the case where app.run(...) returns normally:

cpp
cancel_source.request_cancel();
async_ctx.stop();

if (async_thread.joinable())
  async_thread.join();
1
2
3
4
5

This ensures the async thread is not left running.

HTTP handlers should not block

Avoid this inside a route:

cpp
app.post("/api/refresh", [](Request &, Response &res)
{
  expensive_work();

  res.json({
    "ok", true
  });
});
1
2
3
4
5
6
7
8

Prefer scheduling the work:

cpp
app.post("/api/refresh", [&async_ctx, state](Request &, Response &res)
{
  async_ctx.post([state]()
  {
    // background update
  });

  res.status(202).json({
    "ok", true,
    "queued", true
  });
});
1
2
3
4
5
6
7
8
9
10
11
12

This makes the route respond quickly.

When to use this pattern

Use this pattern when the app needs:

txt
periodic refresh
background cleanup
cache warming
sync loop
metrics collection
queue polling
file watching
maintenance jobs
1
2
3
4
5
6
7
8

Examples:

txt
refresh product recommendations every minute
clean expired sessions every 10 minutes
sync local data with remote storage
send queued notifications
calculate metrics snapshots
1
2
3
4
5

When not to use this pattern

Do not use this pattern for everything.

For a simple HTTP API with no background work, you do not need vix::async.

For one-off immediate work inside a handler, regular C++ code may be enough.

Use async when there is a real reason:

txt
periodic work
delayed work
background coordination
cancellable long-running task
offloaded blocking work
signal-driven shutdown
1
2
3
4
5
6

Common mistakes

Blocking the HTTP handler

Wrong:

cpp
app.post("/api/refresh", [](Request &, Response &res)
{
  std::this_thread::sleep_for(std::chrono::seconds(5));
  res.json({"ok", true});
});
1
2
3
4
5

Correct:

cpp
app.post("/api/refresh", [&async_ctx](Request &, Response &res)
{
  async_ctx.post([]()
  {
    // background work
  });

  res.status(202).json({
    "ok", true,
    "queued", true
  });
});
1
2
3
4
5
6
7
8
9
10
11
12

Sharing state without synchronization

Wrong:

cpp
state->tick_count += 1;
1

from multiple threads without protection.

Correct:

cpp
std::lock_guard<std::mutex> lock(state->mutex);
state->tick_count += 1;
1
2

Forgetting to stop the async runtime

If the async thread is running, stop it before exiting:

cpp
cancel_source.request_cancel();
async_ctx.stop();

if (async_thread.joinable())
  async_thread.join();
1
2
3
4
5

Calling long work directly in the scheduler

Wrong:

cpp
async_ctx.post([]()
{
  expensive_work();
});
1
2
3
4

Better for expensive work:

cpp
co_await ctx.cpu_pool().submit([]()
{
  return expensive_work();
});
1
2
3
4

Installing JSON parser globally

If a route does not need JSON, do not force it through the JSON parser.

Good:

cpp
app.use("/api/products", middleware::app::json_strict_dev(4096));
1

Risky:

cpp
app.use("/api", middleware::app::json_strict_dev(4096));
1

Keep parsers route-specific.

Production notes

For production apps, consider wrapping the async runtime.

A clean class could own:

txt
io_context
cancel_source
thread
worker task startup
shutdown logic
1
2
3
4
5

Example shape:

cpp
class AsyncRuntime
{
public:
  void start();
  void stop();

  vix::async::core::io_context &context();

private:
  vix::async::core::io_context ctx_;
  vix::async::core::cancel_source cancel_;
  std::thread thread_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13

Then AppBootstrap can look like:

txt
create AppState
create AsyncRuntime
start AsyncRuntime
create App
register middleware
register routes
run App
stop AsyncRuntime
1
2
3
4
5
6
7
8

This keeps main() clean.

Complete test flow

Run:

bash
vix run async_app.cpp
1

Check status:

bash
curl -i http://127.0.0.1:8080/api/status
1

Wait two seconds.

Check again:

bash
curl -i http://127.0.0.1:8080/api/status
1

Trigger manual refresh:

bash
curl -i \
  -X POST http://127.0.0.1:8080/api/refresh
1
2

Check status again:

bash
curl -i http://127.0.0.1:8080/api/status
1

Stop with:

txt
Ctrl+C
1

Summary

Use examples/background-task.md for a standalone worker.

Use this page when async runs inside an application.

The core shape is:

cpp
vix::async::core::io_context async_ctx;
vix::async::core::cancel_source cancel_source;

auto worker =
  background_refresh_worker(
    async_ctx,
    state,
    cancel_source.token()
  );

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

std::thread async_thread([&async_ctx]()
{
  async_ctx.run();
  async_ctx.shutdown();
});

vix::App app;

register_routes(app, state, async_ctx);

app.run(8080);

cancel_source.request_cancel();
async_ctx.stop();
async_thread.join();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

The mental model is:

txt
HTTP handlers
  should stay fast

async runtime
  runs background work

shared state
  connects background work to HTTP routes

cancellation
  stops the worker cleanly
1
2
3
4
5
6
7
8
9
10
11

This gives you a practical base for using Vix Async inside real Vix applications.

Released under the MIT License.

Morty Proxy This is a proxified and sanitized view of the page, visit original site.