tin  1.5.9
coroutines

Purpose

Coroutines are the tin task model for deterministic cooperative workflows. They run beside HSM runtimes, channels, actors, and platform adapters without creating framework-owned threads, heap-backed task frames, or callback registration.

Use coroutine tasks when a system needs workflow code that waits, sleeps, retries, samples, supervises, or sequences typed events:

  • startup and self-test sequences;
  • driver service loops that translate payloads into HSM events;
  • periodic watchdogs and heartbeats;
  • retry loops around bounded channels or actor links;
  • tests and simulations that need deterministic time and wake behavior.

An HSM still owns legal product behavior. A coroutine task owns cooperative workflow around that behavior.

API Entry Points

Execution Model

Coroutine tasks do not run on their own. An executor resumes ready tasks when the application gives the executor a turn:

DriveRuntime runtime{};
tsm::cooperative_executor executor{ runtime };
for (;;) {
poll_inputs();
executor.tick(tsm::ticks(1));
(void)executor.run_ready();
wait_for_interrupt_or_next_tick();
}
Definition: runtime.h:45
constexpr tick_count ticks(tick_rep value) noexcept
Definition: ticks.h:85

run_ready() drains currently ready work. tick() advances task sleeps and delayed runtime events by explicit tick counts. A Linux thread, RTOS task, bare-metal superloop, simulator loop, or unit test can own this execution context.

Declaring Tasks

A task entry is a callable object whose operator() returns tsm::task. Machine definitions declare entries in using tasks = ... so task storage is visible in the type.

struct PrepareDrive {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
co_await tsm::send<Drive::PowerOn>(runtime);
co_await tsm::after_ticks(10);
co_await tsm::send<Drive::EnableOperation>(runtime);
}
};
struct DriveApplication : Drive {
using tasks =
};
Definition: coroutine.h:196
Definition: coroutine.h:43
detail::runtime_impl< Definition, Policy, MachinePolicy > Runtime
Definition: runtime.h:531
sleep_ticks_awaitable after_ticks(tsm::tick_rep ticks) noexcept
Definition: coroutine.h:638
Definition: coroutine.h:1046
Definition: coroutine.h:1143
Definition: coroutine.h:1162

The frame budget is part of the declaration. A coroutine frame that exceeds the budget reports a failure.

Waiting And Waking

Runtime synchronization primitives wake coroutine tasks through the executor ready queue. They do not block an OS thread.

struct ButtonTask {
DriveRuntime& runtime,
std::size_t) const {
for (;;) {
co_await button.wait();
button.reset();
co_await tsm::send<Drive::ButtonPressed>(runtime);
}
}
};
Definition: sync.h:122
void reset() noexcept
Definition: sync.h:135
wait_awaitable wait() noexcept
Definition: sync.h:173

Typical wait surfaces:

  • event_flag wait awaitables for level-triggered task wakeups;
  • signal<T> wait awaitables for latest-value notifications;
  • channel<T, N> receive awaitables for bounded FIFO handoff;
  • mutex<T> lock awaitables for cooperative payload protection;
  • cancellation_token wait awaitables for supervised shutdown.

Interrupt and platform adapters should wake these primitives with the *_from_isr() or notify-style methods where available, then let the executor resume tasks in normal cooperative order.

Multi-Way Waits

select lets a task wait for the first of several cooperative operations. This is useful for driver service loops that need either an interrupt, a timeout, or a cancellation signal.

struct SensorService {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
auto& ctx = runtime.context();
for (;;) {
const auto selected = co_await tsm::select(
ctx.sensor_irq.wait(), tsm::timeout_ticks(10));
if (selected.index == 0U) {
co_await tsm::send<Drive::SampleReady>(runtime);
} else {
co_await tsm::send<Drive::SensorTimeout>(runtime);
}
}
}
};
auto select(Awaitables &&... awaitables)
Definition: coroutine.h:882
timeout_ticks_awaitable timeout_ticks(tsm::tick_rep ticks) noexcept
Definition: coroutine.h:710

The losing wait branch is cancelled before the task resumes. That keeps waiter lists bounded and prevents stale wakeups from resuming a task later.

Time

Coroutine time is explicit tick time. The runtime does not read a wall clock.

struct Heartbeat {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
for (;;) {
co_await tsm::after_ticks(100);
co_await tsm::send<Drive::Heartbeat>(runtime);
}
}
};

Use tsm::every_ticks when a task should wake on a fixed logical period:

struct PeriodicHeartbeat {
template<typename Runtime>
Runtime& runtime,
std::size_t) const {
tsm::every_ticks period{ 100 };
for (;;) {
co_await period;
co_await tsm::send<Drive::Heartbeat>(runtime);
}
}
};
Definition: coroutine.h:656

Host clocks, RTOS ticks, hardware timers, and simulation time are adapters that produce elapsed tsm::tick_count values. The same task can run in a unit test, bare-metal superloop, or target executor when the adapter advances ticks deterministically.

Lifecycle

Declared tasks auto-start when the executor is constructed unless a task entry is configured for explicit start. A single-runtime executor can start, cancel, and inspect a declared task by entry identity:

auto result = executor.start<PrepareDrive{}>();
auto status = executor.task_status<PrepareDrive{}>();
(void)executor.cancel<PrepareDrive{}>();
status
Definition: transport.h:47

Dynamic static slots use the same executor and report the same tsm::spawn_result, tsm::task_status, and tsm::task_failure_reason vocabulary as declared tasks.

Dynamic Static Slots

Some systems need task entries chosen at runtime while still avoiding heap allocation. Reserve static dynamic slots in the machine definition:

struct DriveApplication : Drive {
using tasks =
};
DriveRuntime runtime{};
tsm::cooperative_executor executor{ runtime };
auto spawner = executor.spawner();
(void)spawner.spawn<PrepareDrive{}>();
task_spawner< Executor > spawner
Definition: executor.h:150

The executor reports spawn_result::no_slot, frame_too_large, or allocation_failed when a dynamic task cannot start.

Groups And Cancellation

Task groups give application shells a typed way to supervise related entries:

using startup_tasks = tsm::task_group<PrepareDrive{}, CheckSensors{}>;
executor.start_group(startup_tasks{});
executor.cancel_group(startup_tasks{});
Definition: coroutine.h:1073

For cooperative shutdown inside a task, pass a tsm::cancellation_token or shared cancellation source through the task arguments and have the task observe it at explicit wait points.

Resource Accounting

Task frames, task count, timer slots, and queue slots are included in runtime resource summaries:

static_assert(Resources::task_count == 1);
static_assert(Resources::task_arena_bytes >= 2048);
Definition: resources.h:96

This keeps coroutine workflows reviewable for embedded profiles. A resource contract can reject a runtime configuration at compile time if a task frame, timer slot count, or queue budget exceeds the target profile.

When To Use Coroutines

Use coroutines for sequential cooperative workflows that benefit from co_await:

  • wait for an input, then emit a typed event;
  • sleep for explicit ticks, then retry;
  • run a startup sequence that spans multiple events and delays;
  • supervise a group of periodic service loops.

Use actors when several components need a common stepping surface and explicit typed links. Use HSM transitions for legal product behavior. These surfaces compose: an actor can wrap an HSM runtime, a coroutine task can feed the runtime, and an actor group can step the assembled system.

Related pages: