Theme Catppuccin
Make yourself at homeCatppuccin

A softer light for long learning sessions.

Appearance

Loading your preference…

Dark palette

Choose a dark palette to try it.

Light uses Latte. Your choices stay in this browser.

Back to your source shelf

Inside the companion

Renderer Engineering

Explore bounded image work, allocator lifetimes, cancellation, and Asio coroutines.

Project files 15

Suspend with an execution runtime

Three bounded Asio scenarios: timer success, cancellation, and exception delivery.

121 lines coroutine_main.cpp
// A complete, bounded coroutine experiment using standalone Asio 1.38.2.
// One io_context runner, one operation at a time, no detached work or sleeps.
#include <asio/awaitable.hpp>
#include <asio/bind_cancellation_slot.hpp>
#include <asio/cancellation_signal.hpp>
#include <asio/co_spawn.hpp>
#include <asio/io_context.hpp>
#include <asio/post.hpp>
#include <asio/redirect_error.hpp>
#include <asio/steady_timer.hpp>
#include <asio/this_coro.hpp>
#include <asio/use_awaitable.hpp>
#include <array>
#include <chrono>
#include <exception>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>

namespace {
enum class Mode { succeed, cancel, fail };
enum class Event { started, other_handler, resumed, completed };
struct Control {
  asio::cancellation_signal cancel;
  std::array<Event,4> events{};
  std::size_t count{};
  void record(Event event) {
    if (count==events.size()) throw std::logic_error{"trace exceeded its bound"};
    events[count++]=event;
  }
};
struct Payload { std::string label; };
struct Outcome { bool cancelled{}; std::string label; };

// Named coroutine + value parameters: the frame owns these shared owners.
// The payload has no owner outside the suspended operation after caller.reset().
asio::awaitable<Outcome> wait_owned(std::shared_ptr<const Payload> payload,
                                    std::shared_ptr<Control> control, Mode mode) {
  const auto executor=co_await asio::this_coro::executor;
  asio::steady_timer timer{executor};
  timer.expires_at(mode==Mode::cancel ? std::chrono::steady_clock::time_point::max()
                                    : std::chrono::steady_clock::now());
  control->record(Event::started);
  if (mode==Mode::cancel) {
    // post never invokes this handler inline. There is only one run() thread:
    // the coroutine reaches its pending wait before this handler can execute.
    asio::post(executor,[control] {
      control->record(Event::other_handler);
      control->cancel.emit(asio::cancellation_type::terminal);
    });
  }
  asio::error_code error;
  co_await timer.async_wait(asio::redirect_error(asio::use_awaitable,error));
  control->record(Event::resumed);
  if (error==asio::error::operation_aborted) co_return Outcome{true,payload->label};
  if (error) throw asio::system_error{error};
  if (mode==Mode::fail) throw std::runtime_error{"injected preparation failure"};
  co_return Outcome{false,payload->label};
}
void check(bool condition,const char* message) {
  if (!condition) throw std::runtime_error{message};
}
void run_case(Mode mode) {
  asio::io_context context;
  auto control=std::make_shared<Control>();
  auto caller=std::make_shared<const Payload>(Payload{"owned study"});
  const std::weak_ptr<const Payload> probe=caller;
  std::exception_ptr failure;
  Outcome result;
  std::size_t completions{};
  // This is an ordinary completion handler, NOT a capturing coroutine lambda.
  // References stay valid because run() drains the operation before return.
  asio::co_spawn(context,wait_owned(caller,control,mode),
    asio::bind_cancellation_slot(control->cancel.slot(),
      [&](std::exception_ptr error,Outcome value) {
        ++completions;
        failure=error;
        result=std::move(value);
        control->record(Event::completed);
      }));
  caller.reset();
  check(!probe.expired(),"suspended operation must own its payload");
  context.run(); // Graceful drain: do not use stop() to skip cleanup completions.
  check(completions==1,"one completion must be observed");
  check(probe.expired(),"frame/payload must be released after completion");
  check(control->events[0]==Event::started,"coroutine entry recorded");
  check(control->events[control->count-1]==Event::completed,"completion recorded last");
  if (mode==Mode::fail) {
    check(static_cast<bool>(failure),"coroutine exception must reach completion");
    try { std::rethrow_exception(failure); }
    catch (const std::runtime_error& error) {
      check(std::string{error.what()}=="injected preparation failure","expected injected exception");
    }
    std::cout << "failure: exception observed; payload released\n";
  } else {
    check(!failure,"unexpected coroutine exception");
    check(result.label=="owned study","owned input survives suspension");
    check(result.cancelled==(mode==Mode::cancel),"expected terminal outcome");
    if (mode==Mode::cancel) {
      check(control->count==4 && control->events[1]==Event::other_handler &&
            control->events[2]==Event::resumed,"wait -> other handler -> cancelled resume -> completion");
      std::cout << "cancel: started -> other handler -> aborted wait -> completion; payload released\n";
    } else std::cout << "success: expired wait -> completion; payload released\n";
  }
}
}
int main() {
  try {
    run_case(Mode::succeed);
    run_case(Mode::cancel);
    run_case(Mode::fail);
    std::cout << "3 bounded coroutine scenarios passed\n";
    return 0;
  } catch (const std::exception& error) {
    std::cerr << "coroutine experiment: " << error.what() << '\n';
    return 1;
  }
}

This is the file included with the course. Copy and download give you its complete source.

Taking your work offline? Download all course companions