@@ -22,14 +22,12 @@ performance.
2222> 1.0.0 release.
2323
2424``` C++
25- #include < cassert>
26-
2725#include < chrono>
28- #include < coroutine>
2926#include < print>
3027#include < thread>
3128
3229import async_context;
30+ import async_context.schedulers;
3331
3432using namespace std ::chrono_literals;
3533
@@ -77,38 +75,45 @@ async::future<void> sensor_pipeline(async::context& ctx,
7775 std::println("Pipeline '{}' complete!\n", p_name);
7876}
7977
78+ // Unblocks any I/O-blocked context in ctx1 or ctx2 (simulates hardware
79+ // callbacks completing). Runs as a third coroutine alongside the pipelines.
80+ async::future<void > io_unblock_driver(async::context& p_ctx,
81+ async::context& ctx1,
82+ async::context& ctx2)
83+ {
84+ while (true) {
85+ if (ctx1.done() && ctx2.done()) {
86+ co_return;
87+ }
88+ if (ctx1.state() == async::blocked_by::io) {
89+ ctx1.unblock();
90+ }
91+ if (ctx2.state() == async::blocked_by::io) {
92+ ctx2.unblock();
93+ }
94+ co_await 1us;
95+ }
96+ }
97+
8098int main()
8199{
82- // Create context and add them to the scheduler
83- basic_context<8192> ctx1(scheduler) ;
84- basic_context<8192> ctx2(scheduler) ;
100+ async::inplace_context<512> ctx1;
101+ async::inplace_context<512> ctx2 ;
102+ async::inplace_context<512> driver_ctx ;
85103
86- // Run two independent pipelines concurrently
104+ // Start the two pipelines and the I/O unblock driver
87105 auto pipeline1 = sensor_pipeline(ctx1, "🌟 System 1");
88106 auto pipeline2 = sensor_pipeline(ctx2, "🔥 System 2");
107+ auto driver = io_unblock_driver(driver_ctx, ctx1, ctx2);
89108
90- // Round robin between each context
91- while (true) {
92- bool all_done = true;
93- for (auto& ctx : std::to_array({&ctx1, &ctx2}) {
94- if (not ctx->done()) {
95- all_done = false;
96- if (ctx->state() == async::blocked_by::nothing) {
97- ctx->resume();
98- }
99- if (ctx->state() == async::blocked_by::time) {
100- std::this_thread::sleep(ctx.pending_delay());
101- ctx.unblock();
102- }
103- }
104- }
105- if (all_done) {
106- break;
107- }
108- }
109-
110- assert(pipeline1.done());
111- assert(pipeline2.done());
109+ // Drive all three contexts to completion.
110+ // run_until_done sleeps until the nearest time deadline when all contexts
111+ // are blocked, and wakes immediately when any context becomes ready.
112+ async::chrono_clock_adapter< std::chrono::steady_clock > clk;
113+ async::run_until_done(
114+ clk,
115+ [ ] (auto p_wake_time) { std::this_thread::sleep_until(p_wake_time); },
116+ ctx1, ctx2, driver_ctx);
112117
113118 std::println("Both pipelines completed successfully!");
114119 return 0;
@@ -286,6 +291,92 @@ The state of this can be found from the `async::context::state()`. All states
286291besides time are safe to resume at any point. If a context has been blocked by
287292time, then it must defer calling resume until that time has elapsed.
288293
294+ ---
295+
296+ > [!NOTE]
297+ > The following types are provided by the `async_context.schedulers` module.
298+ > Add `import async_context.schedulers;` alongside `import async_context;` to
299+ > use them.
300+
301+ ### `async::clock` (concept)
302+
303+ An instance-based clock concept. Unlike `std::chrono` clocks which require a
304+ static `now()`, `async::clock` requires an instance method so hardware clocks
305+ can be injected as runtime objects. A conforming type must provide:
306+
307+ - `time_point` — the type returned by `now()`
308+ - `duration` — the difference type of two `time_point`s
309+ - `now() const` — returns the current `time_point`
310+ - `time_point` arithmetic: subtraction yields `duration`, addition of
311+ `duration` yields `time_point`
312+ - `time_point::max()` — sentinel meaning "never wake"
313+
314+ ### `async::chrono_clock_adapter<ChronoClock>`
315+
316+ A zero-size adapter that wraps any `std::chrono`-conforming clock (with a
317+ static `now()`) into an `async::clock` (with an instance `now()`). Because it
318+ holds no state, it is always optimized away entirely by the compiler.
319+
320+ ```cpp
321+ async::chrono_clock_adapter<std::chrono::steady_clock> clk;
322+ static_assert(async::clock<decltype(clk)>);
323+
324+ auto now = clk.now(); // forwards to std::chrono::steady_clock::now()
325+ ```
326+
327+ Use this on hosted platforms. On bare-metal, implement ` async::clock ` directly
328+ against your hardware timer peripheral.
329+
330+ ### ` async::run_until_done `
331+
332+ Drives a fixed set of ` async::context ` objects to completion in a cooperative
333+ scheduling loop. On each iteration it resumes every context that is ready or
334+ whose time deadline has elapsed. When all remaining contexts are blocked, it
335+ calls the user-supplied ` p_sleep_until ` callable to suspend the CPU until the
336+ nearest deadline.
337+
338+ ``` cpp
339+ // Without interruptible sleep
340+ async::run_until_done (
341+ clk,
342+ [ ] (auto p_wake_time) { std::this_thread::sleep_until(p_wake_time); },
343+ ctx0, ctx1, ctx2);
344+ ```
345+
346+ An overload accepts an `async::unblock_listener` as a third argument. The
347+ listener is registered on every context so that an I/O completion or ISR can
348+ wake `p_sleep_until` early, avoiding unnecessary latency when all contexts are
349+ time-blocked but an I/O event arrives before the deadline.
350+
351+ ```cpp
352+ async::run_until_done(
353+ clk,
354+ [](auto p_wake_time) {
355+ // sleep until the deadline OR until woken by the listener below
356+ platform_sleep_until(p_wake_time);
357+ },
358+ async::unblock_listener::from([](async::context& p_ctx) noexcept {
359+ // called from ISR/thread when any context is unblocked
360+ platform_wake_from_sleep();
361+ }),
362+ ctx0, ctx1, ctx2);
363+ ```
364+
365+ Key properties:
366+
367+ - ** Stack-allocated scheduler table** — no heap allocation; all internal
368+ bookkeeping lives on the call stack and is destroyed when the function
369+ returns, even on exception
370+ - ** Listener lifetime safety** — every context's listener registration is
371+ cleared in the destructor of the internal scheduler entry, so no context
372+ can hold a dangling pointer after ` run_until_done ` returns
373+ - ** Time-only sleep** — ` p_sleep_until ` is only called when all contexts are
374+ time-blocked * and* none are immediately ready; it is never called if work
375+ remains
376+ - ** Exceptions** — any exception that propagates out of a coroutine is
377+ re-thrown from ` run_until_done ` ; all listener registrations are still
378+ cleaned up via RAII before the exception escapes
379+
289380## Usage
290381
291382### Basic Coroutine
@@ -570,4 +661,4 @@ set(BUILD_BENCHMARKS OFF)
570661
571662Apache License 2.0 - See [ LICENSE] ( LICENSE ) for details.
572663
573- Copyright 2024 - 2025 Khalil Estell and the libhal contributors
664+ Copyright 2024 - 2026 Khalil Estell and the libhal contributors
0 commit comments