Skip to content

Latest commit

 

History

History
729 lines (523 loc) · 11.2 KB

File metadata and controls

729 lines (523 loc) · 11.2 KB

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.cpp

Expected output:

value = 42

Why 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 = 42

Void 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 done

Submit 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: canceled

Exceptions

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 failed

Multiple 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 = 42

Run 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 scheduler

This 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 = 42

Thread 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.