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

Quick Start

The async module lets C++20 coroutines suspend while they wait for timers, network operations, signals, or work running on another thread.

A small Vix Async program normally has three parts: an io_context, one or more task<T> coroutines, and a call to run() that drives the scheduler.

Use the public Vix header:

cpp
#include <vix/async.hpp>

For examples that print output:

cpp
#include <vix/print.hpp>

Create a first async program

Create main.cpp:

cpp
#include <chrono>
#include <vix/async.hpp>
#include <vix/print.hpp>

using namespace std::chrono_literals;

vix::async::task<void> run(vix::async::io_context& ctx)
{
  vix::print("waiting");

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

  vix::print("done");
}

int main()
{
  vix::async::io_context ctx;

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

  ctx.run();

  return 0;
}

The task prints waiting, suspends while the timer is active, then resumes and prints done.

The important part is that the timer does not block the scheduler thread while the coroutine is suspended.

Create and start a task

A task<T> is lazy. Calling a coroutine function creates the task, but does not immediately run its body.

cpp
vix::async::task<int> compute()
{
  co_return 42;
}

To run a root task, start it on a scheduler:

cpp
vix::async::io_context ctx;

auto task = compute();

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

ctx.run();

Tasks are move-only, so start() is normally called on a moved task.

Inside another coroutine, use co_await instead:

cpp
vix::async::task<int> compute()
{
  co_return 42;
}

vix::async::task<void> run()
{
  int value = co_await compute();

  vix::print("value:", value);
}

Wait for a timer

Use the timer service when a coroutine needs to wait for time to pass.

cpp
#include <chrono>
#include <vix/async.hpp>
#include <vix/print.hpp>

using namespace std::chrono_literals;

vix::async::task<void> run(vix::async::io_context& ctx)
{
  vix::print("before");

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

  vix::print("after");
}

While this task is suspended, the scheduler can continue running other ready work.

Run several tasks

Tasks can be composed with when_all.

cpp
#include <chrono>
#include <vix/async.hpp>
#include <vix/print.hpp>

using namespace std::chrono_literals;

vix::async::task<int> first(vix::async::io_context& ctx)
{
  co_await ctx.timers().sleep_for(50ms);
  co_return 10;
}

vix::async::task<int> second(vix::async::io_context& ctx)
{
  co_await ctx.timers().sleep_for(100ms);
  co_return 20;
}

vix::async::task<void> run(vix::async::io_context& ctx)
{
  auto results = co_await vix::async::when_all(
    ctx.get_scheduler(),
    first(ctx),
    second(ctx)
  );

  vix::print("first:", std::get<0>(results));
  vix::print("second:", std::get<1>(results));
}

when_all starts the supplied tasks and resumes when all of them have completed.

Use when_any when only the first completion matters. The other tasks continue running after when_any returns, so the runtime they use must remain alive until they can finish.

Move CPU work off the scheduler

CPU-intensive or blocking work should not occupy the scheduler thread for a long time.

Use the context CPU pool:

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

vix::async::task<void> run(vix::async::io_context& ctx)
{
  int result = co_await ctx.cpu_pool().submit([](){
    int total = 0;

    for (int i = 0; i < 100000; ++i)
    {
      total += i;
    }

    return total;
  });

  vix::print("result:", result);
}

The callable executes on a worker thread. The coroutine suspends while the work runs, then continues with the returned value.

If no coroutine result is needed, use post() instead.

cpp
bool accepted = ctx.cpu_pool().post([](){
  vix::print("background work");
});

vix::print("accepted:", accepted);

Cancel a supported operation

Cancellation is cooperative.

Create a source and pass its token to an operation that supports cancellation:

cpp
vix::async::cancel_source source;

auto token = source.token();

Cancellation can then be requested from another part of the program:

cpp
source.cancel();

The operation decides how to react at its cancellation boundary. Cancellation does not forcibly interrupt arbitrary C++ code that is already executing.

Timers, signals, and supported network operations participate in this model.

Use networking

Networking is also exposed through io_context.

cpp
auto& net = ctx.net();

The network service provides asynchronous TCP, UDP, and DNS operations.

The normal flow remains the same:

text
start operation

coroutine suspends

network work completes

continuation returns to scheduler

coroutine resumes

The focused networking pages cover sockets, acceptors, datagrams, resolution, cancellation, and shutdown behavior.

Runtime lifetime

The io_context should remain alive while tasks and services that depend on it are still active.

A typical program follows this order:

text
create io_context

create root task

start task

run scheduler

finish asynchronous work

shutdown

stop() and shutdown() are not the same operation. stop() requests scheduler termination. shutdown() coordinates the services owned by the context before the runtime finishes.

The full lifecycle is covered in Lifecycle and Shutdown.

Next step

Continue with Core Concepts to understand how task<T>, scheduler, and io_context fit together.

Then move to the focused pages for:

Released under the MIT License.