@@ -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,90 @@ 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+ ## Schedulers
295+
296+ You can import the schedulers by importing `async_context.schedulers`. Importing
297+ `async_context.schedulers` also transitively imports `async_context`.
298+
299+ ### `async::clock` (concept)
300+
301+ An instance-based clock concept. Unlike `std::chrono` clocks which require a
302+ static `now()`, `async::clock` requires an instance method so hardware clocks
303+ can be injected as runtime objects. A conforming type must provide:
304+
305+ - `time_point` — the type returned by `now()`
306+ - `duration` — the difference type of two `time_point`s
307+ - `now() const` — returns the current `time_point`
308+ - `time_point` arithmetic: subtraction yields `duration`, addition of
309+ `duration` yields `time_point`
310+ - `time_point::max()` — sentinel meaning "never wake"
311+
312+ ### `async::chrono_clock_adapter<ChronoClock>`
313+
314+ A zero-size adapter that wraps any `std::chrono`-conforming clock (with a
315+ static `now()`) into an `async::clock` (with an instance `now()`). Because it
316+ holds no state, it is always optimized away entirely by the compiler.
317+
318+ ```cpp
319+ async::chrono_clock_adapter<std::chrono::steady_clock> clk;
320+ static_assert(async::clock<decltype(clk)>);
321+
322+ auto now = clk.now(); // forwards to std::chrono::steady_clock::now()
323+ ```
324+
325+ Use this on hosted platforms. On bare-metal, implement ` async::clock ` directly
326+ against your hardware timer peripheral.
327+
328+ ### ` async::run_until_done `
329+
330+ Drives a fixed set of ` async::context ` objects to completion in a cooperative
331+ scheduling loop. On each iteration it resumes every context that is ready or
332+ whose time deadline has elapsed. When all remaining contexts are blocked, it
333+ calls the user-supplied ` p_sleep_until ` callable to suspend the CPU until the
334+ nearest deadline.
335+
336+ ``` cpp
337+ // Without interruptible sleep
338+ async::run_until_done (
339+ clk,
340+ [ ] (auto p_wake_time) { std::this_thread::sleep_until(p_wake_time); },
341+ ctx0, ctx1, ctx2);
342+ ```
343+
344+ An overload accepts an `async::unblock_listener` as a third argument. The
345+ listener is registered on every context so that an I/O completion or ISR can
346+ wake `p_sleep_until` early, avoiding unnecessary latency when all contexts are
347+ time-blocked but an I/O event arrives before the deadline.
348+
349+ ```cpp
350+ async::run_until_done(
351+ clk,
352+ [](auto p_wake_time) {
353+ // sleep until the deadline OR until woken by the listener below
354+ platform_sleep_until(p_wake_time);
355+ },
356+ async::unblock_listener::from([](async::context& p_ctx) noexcept {
357+ // called from ISR/thread when any context is unblocked
358+ platform_wake_from_sleep();
359+ }),
360+ ctx0, ctx1, ctx2);
361+ ```
362+
363+ Key properties:
364+
365+ - ** Stack-allocated scheduler table** — no heap allocation; all internal
366+ bookkeeping lives on the call stack and is destroyed when the function
367+ returns, even on exception
368+ - ** Listener lifetime safety** — every context's listener registration is
369+ cleared in the destructor of the internal scheduler entry, so no context
370+ can hold a dangling pointer after ` run_until_done ` returns
371+ - ** Time-only sleep** — ` p_sleep_until ` is only called when all contexts are
372+ time-blocked * and* none are immediately ready; it is never called if work
373+ remains
374+ - ** Exceptions** — any exception that propagates out of a coroutine is
375+ re-thrown from ` run_until_done ` ; all listener registrations are still
376+ cleaned up via RAII before the exception escapes
377+
289378## Usage
290379
291380### Basic Coroutine
@@ -570,4 +659,4 @@ set(BUILD_BENCHMARKS OFF)
570659
571660Apache License 2.0 - See [ LICENSE] ( LICENSE ) for details.
572661
573- Copyright 2024 - 2025 Khalil Estell and the libhal contributors
662+ Copyright 2024 - 2026 Khalil Estell and the libhal contributors
0 commit comments