Skip to content

Commit 1436e46

Browse files
committed
Add run_until_done
1 parent 2760b92 commit 1436e46

11 files changed

Lines changed: 675 additions & 137 deletions

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ project(async_context LANGUAGES CXX)
2121
find_package(LibhalCMakeUtil REQUIRED)
2222

2323
libhal_project_init()
24-
libhal_add_library(async_context MODULES modules/async_context.cppm)
24+
libhal_add_library(async_context
25+
MODULES
26+
modules/async_context.cppm
27+
modules/schedulers.cppm)
2528
libhal_apply_compile_options(async_context)
2629
libhal_install_library(async_context NAMESPACE libhal)
2730
libhal_add_tests(async_context
@@ -35,6 +38,8 @@ libhal_add_tests(async_context
3538
basics_dep_inject
3639
on_unblock
3740
simple_scheduler
41+
clock_adapter
42+
run_until_done
3843

3944
MODULES
4045
tests/util.cppm

README.md

Lines changed: 119 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3229
import async_context;
30+
import async_context.schedulers;
3331

3432
using 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+
8098
int 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
286291
besides time are safe to resume at any point. If a context has been blocked by
287292
time, 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

571660
Apache 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

cspell.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,8 @@
5151
"doxygenfunction",
5252
"doxygenenum",
5353
"alignof",
54-
"inplace"
54+
"inplace",
55+
"interruptible"
5556
],
5657
"ignorePaths": [
5758
"build/",

modules/async_context.cppm

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2024 - 2025 Khalil Estell and the libhal contributors
1+
// Copyright 2024 - 2026 Khalil Estell and the libhal contributors
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -333,7 +333,7 @@ public:
333333
}
334334

335335
private:
336-
void on_unblock(async::context const& p_context) noexcept override
336+
void on_unblock(async::context& p_context) noexcept override
337337
{
338338
handler(p_context);
339339
}
@@ -362,7 +362,7 @@ private:
362362
* @note This method MUST be noexcept and ISR-safe. It may be called from
363363
* any execution context including interrupt handlers.
364364
*/
365-
virtual void on_unblock(context const& p_context) noexcept = 0;
365+
virtual void on_unblock(context& p_context) noexcept = 0;
366366
};
367367

368368
/**
@@ -499,8 +499,6 @@ public:
499499
*/
500500
constexpr void unblock_without_notification() noexcept
501501
{
502-
// We clear this information after the unblock call to allow the unblock
503-
// call to inspect the context's current state.
504502
get_original().m_state = blocked_by::nothing;
505503
get_original().m_sleep_time = sleep_duration::zero();
506504
get_original().m_sync_blocker = nullptr;
@@ -526,6 +524,10 @@ public:
526524
if (get_original().m_listener) {
527525
get_original().m_listener->on_unblock(*this);
528526
}
527+
528+
// We clear this context state information after the unblock listener is
529+
// invoked to allow the unblock listener to inspect the context's current
530+
// state prior to being unblocked.
529531
unblock_without_notification();
530532
}
531533

@@ -664,8 +666,11 @@ public:
664666
*/
665667
void resume()
666668
{
667-
// We cannot resume the a coroutine blocked by time.
668-
// Only the scheduler can unblock a context state.
669+
// We cannot resume the a coroutine blocked by time. Only the scheduler can
670+
// unblock a context state.
671+
//
672+
// This needs to be here to ensure that sync_wait is possible, otherwise the
673+
// blocked_by::time semantic cannot be supported.
669674
if (state() != blocked_by::time) {
670675
m_active_handle.resume();
671676
}
@@ -924,15 +929,16 @@ private:
924929
return coroutine_frame_stack_address;
925930
}
926931

932+
// A concern for this library is how large the context objet is thus the word
933+
// sizes for each field is denoted below.
927934
std::coroutine_handle<> m_active_handle = noop_sentinel; // word 1
928935
uptr* m_stack_pointer = nullptr; // word 2
929936
std::span<uptr> m_stack{}; // word 3-4
930937
context* m_original = nullptr; // word 5
931-
// ----------- Only available from the original -----------
932-
unblock_listener* m_listener = nullptr; // word 6
933-
sleep_duration m_sleep_time = sleep_duration::zero(); // word 7
934-
context* m_sync_blocker = nullptr; // word 8
935-
blocked_by m_state = blocked_by::nothing; // word 9: pad 3
938+
unblock_listener* m_listener = nullptr; // word 6
939+
sleep_duration m_sleep_time = sleep_duration::zero(); // word 7
940+
context* m_sync_blocker = nullptr; // word 8
941+
blocked_by m_state = blocked_by::nothing; // word 9: pad 3
936942
};
937943

938944
/**
@@ -1078,8 +1084,8 @@ public:
10781084

10791085
inplace_context(inplace_context const&) = delete;
10801086
inplace_context& operator=(inplace_context const&) = delete;
1081-
inplace_context(inplace_context&&) = default;
1082-
inplace_context& operator=(inplace_context&&) = default;
1087+
inplace_context(inplace_context&&) = delete;
1088+
inplace_context& operator=(inplace_context&&) = delete;
10831089

10841090
~inplace_context()
10851091
{

0 commit comments

Comments
 (0)