IOFiber


Fibers are lightweight threads managed in user-space. Fibers from this library are:

  • Stackful.

  • Cooperative.

  • Use Boost.Asio execution contexts (e.g. boost::asio::io_context) as schedulers. Any async function will work as a suspension point.

  • Non-symmetric. Main fiber and secondary fibers are exposed to different APIs. There is a clear separation between fibers that run inside and outside ASIO’s execution contexts.

trial::iofiber::fiber is an alias for trial::iofiber::basic_fiber<boost::asio::io_context::strand>. To spawn a fiber, pass a strand executor and a fiber start-function to fiber constructor. Fiber will be scheduled through executor and use it throughout its lifetime and start-function will be executed on top of fiber’s stack:

boost::asio::io_context ctx;
boost::asio::io_context::strand strand{ctx};

fiber(
  strand,
  [](fiber::this_fiber this_fiber) {
    std::cout << "Hello World" << std::endl;
  }
).detach();

ctx.run();

The start-function must have a fiber::this_fiber parameter through which it can have access to local/private[1] functions. There is a second constructor to instantiate a strand for you:

boost::asio::io_context ctx;

fiber(
  ctx,
  [](fiber::this_fiber this_fiber) {
    std::cout << "Hello World" << std::endl;
  }
).detach();

ctx.run();

And there is a third constructor which will reuse the strand from the parent fiber:

boost::asio::io_context ctx;

fiber(
  ctx,
  [](fiber::this_fiber this_fiber) {
    fiber(
      this_fiber,
      [](fiber::this_fiber this_fiber) {
        std::cout << " World" << std::endl;
      }
    ).detach();

    std::cout << "Hello";
  }
).detach();

ctx.run();

All fiber objects must be either join()'ed or detach()'ed. If you don’t call any of these functions, fiber's destructor will call strand.context().stop() function to stop the application (a less severe alternative to std::terminate()). You can check whether the application finished normally or abnormally using trial::iofiber::context_aborted().

Note
Implementation details

It follows a design similar to POSIX threads:

Failure to join with a thread that is joinable (i.e., one that is not detached), produces a "zombie thread". Avoid doing this, since each zombie thread consumes some system resources, and when enough zombie threads have accumulated, it will no longer be possible to create new threads (or processes).

— pthread_join(3)

As of our fibers, each fiber will call strand.on_work_started() and the matching strand.on_work_finished() will only be called when you either join() or detach() a fiber. This behaviour allows you to safely join fibers from other execution contexts even if the foreign execution context runs out of work[2].

std::thread design evolution is also followed by having a movable object whose destructor will stop the world if it detects an error in the code logic.

boost::asio::io_context ioctx;

fiber f1(
  ioctx,
  [](fib::fiber::this_fiber this_fiber) {
    boost::asio::steady_timer timer{this_fiber.get_executor().context()};

    std::cout << "3..." << std::flush;
    timer.expires_after(std::chrono::seconds(1));
    timer.async_wait(this_fiber);

    std::cout << " 2..." << std::flush;
    timer.expires_after(std::chrono::seconds(1));
    timer.async_wait(this_fiber);

    std::cout << " 1..." << std::endl;
    timer.expires_after(std::chrono::seconds(1));
    timer.async_wait(this_fiber);
  }
);

fiber(
  ioctx,
  [&f1](fib::fiber::this_fiber this_fiber) {
    f1.join(this_fiber);

    std::cout << "Hello World" << std::endl;
  }
).detach();

ioctx.run();

fiber::this_fiber

The this_fiber object passed to the fiber start-function is a completion token and you can use it with any Boost.Asio’s async_* function.

void start(fiber::this_fiber this_fiber)
{
  boost::asio::steady_timer timer{this_fiber.get_executor().context()};
  timer.expires_after(std::chrono::seconds(1));
  try {
    timer.async_wait(this_fiber);
    // ...
  } catch (const boost::system::system_error& e) {
    // ...

If you want to handle boost::system::error_code errors directly instead having them translated to exceptions, use the operator[]:

void start(fiber::this_fiber this_fiber)
{
  boost::asio::steady_timer timer{this_fiber.get_executor().context()};
  timer.expires_after(std::chrono::seconds(1));

  boost::system::error_code ec;
  timer.async_wait(this_fiber[ec]);

Another ability this_fiber provides you is the ability to do spurious yields:

this_fiber.yield();

See also:


1. API to fiber management which is only available from within the fiber itself and not through remote/foreign fibers.
2. ASIO’s strands are used extensively to do non-blocking synchronization and access to shared state. In the case of join(), the strand methods will be no-ops by the time boost::asio::io_context::run() returns, so we need to keep’em busy.