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:
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 cleanly2
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:
GET /api/health
GET /api/status
POST /api/refresh2
3
At the same time, an async worker runs in the background every second.
The worker updates shared state:
tick count
last job result
running flag2
3
The HTTP routes can read that state.
Project structure
Create:
async_app_demo/
└── async_app.cpp2
Create the file:
mkdir async_app_demo
cd async_app_demo
touch async_app.cpp2
3
Source
Open:
async_app.cppAdd:
#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;
}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:
vix run async_app.cppThe HTTP server listens on:
http://127.0.0.1:8080The async worker starts in a separate runtime thread.
Test health
curl -i http://127.0.0.1:8080/api/healthExpected body:
{
"ok": true,
"service": "async-app"
}2
3
4
Test background status
curl -i http://127.0.0.1:8080/api/statusExpected body shape:
{
"ok": true,
"worker_running": true,
"tick_count": 3,
"last_result": 30
}2
3
4
5
6
Call it again after a few seconds:
curl -i http://127.0.0.1:8080/api/statusYou should see tick_count and last_result changing.
Trigger one background update from HTTP
curl -i \
-X POST http://127.0.0.1:8080/api/refresh2
Expected status:
202 AcceptedExpected body:
{
"ok": true,
"queued": true
}2
3
4
This route does not run the work directly inside the HTTP handler.
It schedules work into the async runtime:
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;
});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:
std::thread async_thread([&async_ctx]()
{
run_async_runtime(async_ctx);
});2
3
4
The HTTP app runs on the main thread:
app.run(8080);The model is:
main thread
HTTP server
async thread
background worker
timers
CPU pool tasks
signal watcher2
3
4
5
6
7
8
This keeps the example simple and explicit.
Shared state
The HTTP routes and async worker share:
std::shared_ptr<AppState>The state is protected by a mutex:
struct AppState
{
std::mutex mutex{};
int tick_count{0};
int last_result{0};
bool worker_running{false};
};2
3
4
5
6
7
The worker writes the state:
update_background_state(state, tick, result);The HTTP route reads the state:
std::lock_guard<std::mutex> lock(state->mutex);
tick_count = state->tick_count;
last_result = state->last_result;
worker_running = state->worker_running;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:
AsyncRuntime
owns io_context
owns cancel_source
owns thread
starts workers
stops workers
AppState
owns shared state
RouteRegistry
registers HTTP routes
AppBootstrap
wires everything together2
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:
co_await ctx.timers().sleep_for(1s, token);This is better than:
std::this_thread::sleep_for(1s);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:
ctx.cpu_pool().submit(...)Example:
const int result = co_await ctx.cpu_pool().submit([tick]()
{
return compute_background_result(tick);
}, token);2
3
4
Use the CPU pool when the work may be:
CPU-heavy
blocking
synchronous
slow enough to affect the scheduler2
3
4
For tiny work, direct computation is fine.
For real backend work, avoid blocking the scheduler thread.
Cancellation
The app creates:
vix::async::core::cancel_source cancel_source;The worker receives:
cancel_source.token()When the app shuts down, cancellation is requested:
cancel_source.request_cancel();The worker checks:
while (!token.is_cancelled())and cancellable waits use the token:
co_await ctx.timers().sleep_for(1s, token);The rule is:
cancel_source requests shutdown
cancel_token lets tasks observe shutdown2
Signal handling
The async runtime listens for:
SIGINT
SIGTERM2
with:
ctx.signals().add(SIGINT);
ctx.signals().add(SIGTERM);2
Then waits:
const int signal = co_await ctx.signals().async_wait();When a signal is received:
source.request_cancel();This is useful for terminal shutdown:
Ctrl+Cand process managers:
SIGTERMShutdown flow
The shutdown flow is:
Ctrl+C
-> signal task wakes up
-> cancellation requested
-> worker observes cancellation
-> async context stops
-> async thread joins
-> process exits2
3
4
5
6
7
The code also handles the case where app.run(...) returns normally:
cancel_source.request_cancel();
async_ctx.stop();
if (async_thread.joinable())
async_thread.join();2
3
4
5
This ensures the async thread is not left running.
HTTP handlers should not block
Avoid this inside a route:
app.post("/api/refresh", [](Request &, Response &res)
{
expensive_work();
res.json({
"ok", true
});
});2
3
4
5
6
7
8
Prefer scheduling the work:
app.post("/api/refresh", [&async_ctx, state](Request &, Response &res)
{
async_ctx.post([state]()
{
// background update
});
res.status(202).json({
"ok", true,
"queued", true
});
});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:
periodic refresh
background cleanup
cache warming
sync loop
metrics collection
queue polling
file watching
maintenance jobs2
3
4
5
6
7
8
Examples:
refresh product recommendations every minute
clean expired sessions every 10 minutes
sync local data with remote storage
send queued notifications
calculate metrics snapshots2
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:
periodic work
delayed work
background coordination
cancellable long-running task
offloaded blocking work
signal-driven shutdown2
3
4
5
6
Common mistakes
Blocking the HTTP handler
Wrong:
app.post("/api/refresh", [](Request &, Response &res)
{
std::this_thread::sleep_for(std::chrono::seconds(5));
res.json({"ok", true});
});2
3
4
5
Correct:
app.post("/api/refresh", [&async_ctx](Request &, Response &res)
{
async_ctx.post([]()
{
// background work
});
res.status(202).json({
"ok", true,
"queued", true
});
});2
3
4
5
6
7
8
9
10
11
12
Sharing state without synchronization
Wrong:
state->tick_count += 1;from multiple threads without protection.
Correct:
std::lock_guard<std::mutex> lock(state->mutex);
state->tick_count += 1;2
Forgetting to stop the async runtime
If the async thread is running, stop it before exiting:
cancel_source.request_cancel();
async_ctx.stop();
if (async_thread.joinable())
async_thread.join();2
3
4
5
Calling long work directly in the scheduler
Wrong:
async_ctx.post([]()
{
expensive_work();
});2
3
4
Better for expensive work:
co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});2
3
4
Installing JSON parser globally
If a route does not need JSON, do not force it through the JSON parser.
Good:
app.use("/api/products", middleware::app::json_strict_dev(4096));Risky:
app.use("/api", middleware::app::json_strict_dev(4096));Keep parsers route-specific.
Production notes
For production apps, consider wrapping the async runtime.
A clean class could own:
io_context
cancel_source
thread
worker task startup
shutdown logic2
3
4
5
Example shape:
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_;
};2
3
4
5
6
7
8
9
10
11
12
13
Then AppBootstrap can look like:
create AppState
create AsyncRuntime
start AsyncRuntime
create App
register middleware
register routes
run App
stop AsyncRuntime2
3
4
5
6
7
8
This keeps main() clean.
Complete test flow
Run:
vix run async_app.cppCheck status:
curl -i http://127.0.0.1:8080/api/statusWait two seconds.
Check again:
curl -i http://127.0.0.1:8080/api/statusTrigger manual refresh:
curl -i \
-X POST http://127.0.0.1:8080/api/refresh2
Check status again:
curl -i http://127.0.0.1:8080/api/statusStop with:
Ctrl+CSummary
Use examples/background-task.md for a standalone worker.
Use this page when async runs inside an application.
The core shape is:
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();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:
HTTP handlers
should stay fast
async runtime
runs background work
shared state
connects background work to HTTP routes
cancellation
stops the worker cleanly2
3
4
5
6
7
8
9
10
11
This gives you a practical base for using Vix Async inside real Vix applications.