Skip to content

Commit 7f2e086

Browse files
committed
Add run_until_done
1 parent 2760b92 commit 7f2e086

File tree

9 files changed

+669
-124
lines changed

9 files changed

+669
-124
lines changed

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: 121 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,92 @@ 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+
---
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

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

modules/async_context.cppm

Lines changed: 19 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,15 @@ private:
924929
return coroutine_frame_stack_address;
925930
}
926931

932+
// A concern for this library is how large the context objet is.
927933
std::coroutine_handle<> m_active_handle = noop_sentinel; // word 1
928934
uptr* m_stack_pointer = nullptr; // word 2
929935
std::span<uptr> m_stack{}; // word 3-4
930936
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
937+
unblock_listener* m_listener = nullptr; // word 6
938+
sleep_duration m_sleep_time = sleep_duration::zero(); // word 7
939+
context* m_sync_blocker = nullptr; // word 8
940+
blocked_by m_state = blocked_by::nothing; // word 9: pad 3
936941
};
937942

938943
/**
@@ -1078,8 +1083,8 @@ public:
10781083

10791084
inplace_context(inplace_context const&) = delete;
10801085
inplace_context& operator=(inplace_context const&) = delete;
1081-
inplace_context(inplace_context&&) = default;
1082-
inplace_context& operator=(inplace_context&&) = default;
1086+
inplace_context(inplace_context&&) = delete;
1087+
inplace_context& operator=(inplace_context&&) = delete;
10831088

10841089
~inplace_context()
10851090
{

0 commit comments

Comments
 (0)