Thread pool
This guide shows how to use the Vix Async thread pool.
Use this page when you want to run CPU-heavy or blocking work outside the io_context scheduler thread.
Public header
#include <vix/async.hpp>
#include <vix/print.hpp>What the thread pool provides
The thread pool is owned by io_context.
ctx.cpu_pool()It is used to offload work that should not run directly on the scheduler thread.
Use it for:
- CPU-heavy functions
- blocking functions
- synchronous library calls
- work that should resume back into the async flow
Basic example
#include <vix/async.hpp>
#include <vix/print.hpp>
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
int value = co_await ctx.cpu_pool().submit([]()
{
return 21 * 2;
});
vix::print("value =", value);
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Run:
vix run main.cppExpected output:
value = 42Why use the thread pool?
The io_context scheduler should stay responsive.
Avoid doing heavy work directly inside ctx.post(...) or inside a coroutine before an await point.
Wrong:
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
int value = expensive_work();
vix::print("value =", value);
ctx.stop();
co_return;
}Better:
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
int value = co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});
vix::print("value =", value);
ctx.stop();
co_return;
}Submit work and await the result
Use submit inside a coroutine.
int value = co_await ctx.cpu_pool().submit([]()
{
return 42;
});The callable runs on a worker thread. The coroutine resumes back through the owning io_context.
Return a value
The callable can return a value.
auto result = co_await ctx.cpu_pool().submit([]()
{
return 42;
});Complete example:
#include <vix/async.hpp>
#include <vix/print.hpp>
int compute_value()
{
return 21 * 2;
}
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
int value = co_await ctx.cpu_pool().submit([]()
{
return compute_value();
});
vix::print("computed =", value);
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
computed = 42Void work
The callable can also return void.
co_await ctx.cpu_pool().submit([]()
{
do_work();
});Example:
#include <vix/async.hpp>
#include <vix/print.hpp>
void blocking_work()
{
volatile int value = 0;
for (int i = 0; i < 1000; ++i)
{
value += i;
}
}
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
co_await ctx.cpu_pool().submit([]()
{
blocking_work();
});
vix::print("work done");
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
work doneSubmit a plain callback
The thread pool also supports plain callback submission.
ctx.cpu_pool().submit([]()
{
// background callback
});Use this when you do not need to await a result. For coroutine workflows, prefer the awaitable form:
auto value = co_await ctx.cpu_pool().submit([]()
{
return 42;
});Cancellation
submit accepts a cancel_token.
auto value = co_await ctx.cpu_pool().submit([]()
{
return 42;
}, source.token());If the token is already cancelled before the work starts, the operation throws a cancellation error.
#include <vix/async.hpp>
#include <vix/print.hpp>
#include <system_error>
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
vix::async::core::cancel_source source;
source.request_cancel();
try
{
int value = co_await ctx.cpu_pool().submit([]()
{
return 42;
}, source.token());
vix::print("value =", value);
}
catch (const std::system_error &ex)
{
vix::eprint("cancelled:", ex.code().message());
}
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
cancelled: canceledExceptions
If the callable throws, the exception is captured and rethrown when the coroutine resumes.
#include <vix/async.hpp>
#include <vix/print.hpp>
#include <exception>
#include <stdexcept>
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
try
{
int value = co_await ctx.cpu_pool().submit([]()
{
throw std::runtime_error("worker failed");
return 42;
});
vix::print("value =", value);
}
catch (const std::exception &ex)
{
vix::eprint(ex.what());
}
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
worker failedMultiple jobs
You can await multiple pool jobs from one coroutine.
#include <vix/async.hpp>
#include <vix/print.hpp>
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
int a = co_await ctx.cpu_pool().submit([]()
{
return 20;
});
int b = co_await ctx.cpu_pool().submit([]()
{
return 22;
});
vix::print("sum =", a + b);
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
sum = 42Run background work then resume
A thread pool job runs away from the scheduler. After the job completes, the coroutine resumes on the io_context scheduler.
Typical flow:
coroutine starts on scheduler
co_await cpu_pool.submit(...)
callable runs on worker thread
result is stored
coroutine resumes on schedulerThis keeps async code simple while moving heavy work off the runtime thread.
Thread pool lifecycle
The pool is created lazily.
ctx.cpu_pool()It is owned by io_context. When the context is shut down, the pool is released.
ctx.shutdown();You normally do not create the pool manually. Use:
ctx.cpu_pool()Blocking work example
This example simulates blocking work.
#include <vix/async.hpp>
#include <vix/print.hpp>
#include <chrono>
#include <thread>
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
vix::print("before blocking work");
int value = co_await ctx.cpu_pool().submit([]()
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
return 42;
});
vix::print("after blocking work");
vix::print("value =", value);
ctx.stop();
co_return;
}
int main()
{
vix::async::core::io_context ctx;
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();
return 0;
}Expected output:
before blocking work
after blocking work
value = 42Thread pool vs timers
Use timers for waiting.
co_await ctx.timers().sleep_for(std::chrono::milliseconds(100));Use the thread pool for work.
auto value = co_await ctx.cpu_pool().submit([]()
{
return compute_value();
});Do not use the thread pool just to sleep.
Thread pool vs post
Use post for small scheduler callbacks.
ctx.post([&ctx]()
{
vix::print("callback");
ctx.stop();
});Use the thread pool for work that should not block the scheduler.
auto value = co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});Common workflows
Run CPU work
int value = co_await ctx.cpu_pool().submit([]()
{
return compute_value();
});Run blocking work
auto result = co_await ctx.cpu_pool().submit([]()
{
return blocking_call();
});Run void work
co_await ctx.cpu_pool().submit([]()
{
write_file();
});Use cancellation
vix::async::core::cancel_source source;
auto value = co_await ctx.cpu_pool().submit([]()
{
return 42;
}, source.token());Catch worker exceptions
try
{
auto value = co_await ctx.cpu_pool().submit([]()
{
return risky_work();
});
vix::print("value =", value);
}
catch (const std::exception &ex)
{
vix::eprint(ex.what());
}Common mistakes
Running heavy work on the scheduler
Wrong:
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
auto value = expensive_work();
vix::print("value =", value);
ctx.stop();
co_return;
}Correct:
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
auto value = co_await ctx.cpu_pool().submit([]()
{
return expensive_work();
});
vix::print("value =", value);
ctx.stop();
co_return;
}Forgetting to await submit
Wrong:
ctx.cpu_pool().submit([]()
{
return 42;
});Correct:
int value = co_await ctx.cpu_pool().submit([]()
{
return 42;
});Use plain submit(std::function<void()>) only when you intentionally want callback-style background work.
Using the thread pool for timers
Wrong:
co_await ctx.cpu_pool().submit([]()
{
std::this_thread::sleep_for(std::chrono::seconds(1));
});Correct:
co_await ctx.timers().sleep_for(std::chrono::seconds(1));Forgetting to run the context
Wrong:
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());Correct:
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
ctx.run();Forgetting to stop the context
The main async task should stop the context when done.
ctx.stop();
co_return;Expecting cancellation to stop a running function
Cancellation is checked before execution starts. If the callable is already running, it must cooperate manually.
if (token.is_cancelled())
{
return;
}Best practices
Use the thread pool for blocking or CPU-heavy work. Keep scheduler callbacks short. Prefer co_await ctx.cpu_pool().submit(...) inside coroutine tasks. Catch exceptions around awaited worker jobs. Use cancellation tokens when the work may become unnecessary. Use timers for delays, not the thread pool. Call ctx.stop() when the main async flow is complete.
Related pages
| Page | Purpose |
|---|---|
| io_context | Learn the runtime context. |
| Tasks | Learn coroutine tasks. |
| Spawn | Learn how to start async work. |
| Timers | Learn delays and timer callbacks. |
| Cancellation | Learn cancellation tokens. |
| when_all / when_any | Learn task composition. |
| API Reference | See the public API surface. |
Next step
Continue with when_all and when_any.