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

Spawn

This guide shows how to start async tasks with Vix Async.

Use this page when you want to launch a coroutine task from main, run detached async work, or schedule work on an io_context.

Public header

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

What spawn means

In Vix Async, tasks are lazy.

Creating a task does not run it.

cpp
auto t = app(ctx);
1

To run the task, you must schedule it on a scheduler.

cpp
std::move(t).start(ctx.get_scheduler());
1

This starts the coroutine on the io_context scheduler.

Basic task startup

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

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  vix::print("app started");

  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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Run:

vix run main.cpp
1

Expected output:

app started
1

Start on the scheduler

The most direct way to start a top-level task is:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
1
2

Then run the context:

cpp
ctx.run();
1

Full pattern:

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

auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

ctx.run();
1
2
3
4
5
6

Why std::move is used

task<T> is move-only.

Starting a task transfers ownership of the coroutine frame to the scheduler.

cpp
std::move(t).start(ctx.get_scheduler());
1

After this, do not use t again.

Top-level task

A common pattern is to write one top-level app task.

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  vix::print("running");

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7

Then start it from main.

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

  auto t = app(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11

Spawn with timer work

You can start a task that uses timers.

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

#include <chrono>

using namespace std::chrono_literals;

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

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

  vix::print("after sleep");

  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;
}
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

Expected output:

before sleep
after sleep
1
2

Spawn with returned values

A started task can await other tasks.

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

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

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  const int value = co_await compute();

  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;
}
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

Expected output:

value = 42
1

Fire-and-forget work

Use detached async work when you do not need to await the task result directly.

A detached task is useful for background work that will stop the context later.

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

#include <chrono>

using namespace std::chrono_literals;

vix::async::core::task<void> background(vix::async::core::io_context &ctx)
{
  co_await ctx.timers().sleep_for(100ms);

  vix::print("background done");

  ctx.stop();
  co_return;
}

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

  auto t = background(ctx);
  std::move(t).start(ctx.get_scheduler());

  ctx.run();

  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

Expected output:

background done
1

Multiple tasks

You can start multiple top-level tasks.

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

#include <chrono>

using namespace std::chrono_literals;

vix::async::core::task<void> first(vix::async::core::io_context &ctx)
{
  co_await ctx.timers().sleep_for(50ms);

  vix::print("first");
  co_return;
}

vix::async::core::task<void> second(vix::async::core::io_context &ctx)
{
  co_await ctx.timers().sleep_for(100ms);

  vix::print("second");
  ctx.stop();
  co_return;
}

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

  auto a = first(ctx);
  auto b = second(ctx);

  std::move(a).start(ctx.get_scheduler());
  std::move(b).start(ctx.get_scheduler());

  ctx.run();

  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

Expected output:

first
second
1
2

Using post for simple callbacks

For simple callback 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 callback");
    ctx.stop();
  });

  ctx.run();

  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Expected output:

posted callback
1

Use task when the work needs co_await. Use post when the work is a simple callback.

Start vs co_await

Use start at the top level.

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
1
2

Use co_await inside another task.

cpp
const int value = co_await compute();
1

Typical layout:

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

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  const int value = co_await compute();

  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;
}
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

Stopping the context

A started task should usually stop the context when the main async work is done.

cpp
ctx.stop();
co_return;
1
2

Without ctx.stop(), ctx.run() may keep waiting for work.

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  vix::print("done");

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7

Error handling

Exceptions thrown inside an awaited task are rethrown to the awaiting coroutine.

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

#include <exception>
#include <stdexcept>

vix::async::core::task<int> fail()
{
  throw std::runtime_error("failed");
  co_return 0;
}

vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  try
  {
    const int value = co_await fail();
    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;
}
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

Expected output:

failed
1

Common workflows

Start one task

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

ctx.run();
1
2
3
4

Start two tasks

cpp
auto a = first(ctx);
auto b = second(ctx);

std::move(a).start(ctx.get_scheduler());
std::move(b).start(ctx.get_scheduler());

ctx.run();
1
2
3
4
5
6
7

Start a task that waits on a timer

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  co_await ctx.timers().sleep_for(std::chrono::milliseconds(100));

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7

Start a task that awaits another task

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  const int value = co_await compute();

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

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7
8
9

Use post for callbacks

cpp
ctx.post([&ctx]()
{
  vix::print("callback");
  ctx.stop();
});
1
2
3
4
5

Common mistakes

Creating a task but not starting it

Wrong:

cpp
auto t = app(ctx);

ctx.run();
1
2
3

Correct:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

ctx.run();
1
2
3
4

Starting a task but not running the context

Wrong:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());
1
2

Correct:

cpp
auto t = app(ctx);
std::move(t).start(ctx.get_scheduler());

ctx.run();
1
2
3
4

Forgetting std::move

Wrong:

cpp
t.start(ctx.get_scheduler());
1

Correct:

cpp
std::move(t).start(ctx.get_scheduler());
1

Using a task after starting it

Wrong:

cpp
auto t = app(ctx);

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

if (t.valid())
{
  vix::print("still valid");
}
1
2
3
4
5
6
7
8

Correct: after start, treat the task as transferred.

Forgetting to stop the context

Wrong:

cpp
vix::async::core::task<void> app(vix::async::core::io_context &)
{
  vix::print("done");
  co_return;
}
1
2
3
4
5

Correct:

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  vix::print("done");

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7

Using post when you need co_await

Wrong:

cpp
ctx.post([&ctx]()
{
  co_await ctx.timers().sleep_for(std::chrono::milliseconds(100));
});
1
2
3
4

Correct:

cpp
vix::async::core::task<void> app(vix::async::core::io_context &ctx)
{
  co_await ctx.timers().sleep_for(std::chrono::milliseconds(100));

  ctx.stop();
  co_return;
}
1
2
3
4
5
6
7

Best practices

Use one top-level task<void> for the main async application. Start the top-level task from main. Use co_await to compose tasks inside other tasks. Use ctx.post only for simple callbacks. Call ctx.stop() when the top-level async flow is complete. Do not use a task after moving it into start. Keep detached work small and observable.

PagePurpose
io_contextLearn the runtime context and scheduler loop.
TasksLearn task<T> and task<void>.
TimersLearn coroutine sleeps and delayed callbacks.
CancellationLearn cooperative cancellation.
Thread poolLearn background execution.
when_all / when_anyLearn task composition.
API ReferenceSee the public API surface.

Next step

Continue with timers.

Released under the MIT License.

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