time_loop¶
coio::time_loop is coio's portable execution context: a manually driven event loop with a timer queue and no I/O backend. It runs everywhere the library compiles and is the natural choice for worker threads, testing, and timer-driven logic.
Header: #include <coio/execution_context.h>
Overview¶
time_loop implements the full shared execution-context model: MPSC threading, run/poll driving, work tracking, request_stop, and a scheduler with schedule(), schedule_at(), schedule_after() and now(). What distinguishes it from the I/O contexts is only what it lacks: there is no make_io_object, so it cannot host sockets, files or pipes.
When idle with work outstanding, run()/run_one() block on an internal semaphore with a deadline taken from the earliest queued timer — the loop wakes exactly when the next timer is due or when another thread submits work.
Synopsis¶
namespace coio {
class time_loop {
public:
struct scheduler; // models execution::scheduler; see below
template<typename T = void, typename Alloc = void>
using task = coio::task<T, Alloc, scheduler>;
explicit time_loop(std::pmr::memory_resource& resource = *std::pmr::get_default_resource()) noexcept;
time_loop(const time_loop&) = delete;
~time_loop();
auto operator= (const time_loop&) -> time_loop& = delete;
[[nodiscard]] auto get_scheduler() noexcept -> scheduler;
[[nodiscard]] auto get_allocator() const noexcept -> std::pmr::polymorphic_allocator<>;
auto request_stop() -> void;
auto work_started() noexcept -> void;
auto work_finished() noexcept -> void;
auto run() -> std::size_t;
auto run_one() -> bool;
auto poll() -> std::size_t;
auto poll_one() -> bool;
};
struct time_loop::scheduler {
using scheduler_concept = execution::scheduler_tag;
[[nodiscard]] static auto now() noexcept -> std::chrono::steady_clock::time_point;
[[nodiscard]] auto schedule() const noexcept -> /*sender of ()*/;
[[nodiscard]] auto schedule_at(std::chrono::steady_clock::time_point deadline) const noexcept
-> /*sender of ()*/;
template<typename Rep, typename Period>
[[nodiscard]] auto schedule_after(std::chrono::duration<Rep, Period> duration) const noexcept
-> /*sender of ()*/;
[[nodiscard]] auto context() const noexcept -> time_loop&;
friend auto operator== (const scheduler&, const scheduler&) -> bool;
};
}
API Reference¶
Construction / destruction¶
explicit time_loop(std::pmr::memory_resource& resource = *std::pmr::get_default_resource()) noexcept;
~time_loop();
Construction takes an optional std::pmr::memory_resource backing the loop's internal allocations, defaulting to std::pmr::get_default_resource(); the resource must outlive the context. The context is neither copyable nor movable. Destroying a time_loop with outstanding work calls std::terminate() — see destructor semantics.
scheduler¶
time_loop::scheduler provides exactly the common scheduler operations: schedule(), schedule_at(), schedule_after(), static now(), context(), the get_forward_progress_guarantee (parallel) and get_allocator queries, and equality (equal iff same time_loop). Its senders complete on the loop's consumer thread and advertise this via get_completion_scheduler for all three CPOs.
schedule_at/schedule_after senders are cancellable both by the receiver's stop token and by time_loop::request_stop().
task alias¶
A task affine to a time_loop: awaited senders always resume the coroutine on the loop's consumer thread, with no type-erasure overhead compared to the default coio::task<T>. Start one with starts_on(loop.get_scheduler(), ...) or by awaiting it from another task on the same loop.
Driving, work tracking, stopping¶
run(), run_one(), poll(), poll_one(), work_started(), work_finished() and request_stop() behave exactly as specified on the contexts page. In short: one consumer thread at a time; run() returns the number of completions processed once no work remains; request_stop() cancels outstanding timed waits so the loop drains.
Example¶
Two timed jobs racing on one loop, driven from the main thread (adapted from examples/hello.cpp):
#include <chrono>
#include <iostream>
#include <coio/core.h>
#include <coio/execution_context.h>
using namespace std::chrono_literals;
auto job(coio::time_loop::scheduler sched, std::string_view name, int value,
std::chrono::seconds timeout) -> coio::task<int> {
co_await sched.schedule_after(timeout); // timed wait on the loop
std::cout << name << " completed\n";
co_return value;
}
auto main() -> int {
coio::time_loop context;
const auto tick = std::chrono::steady_clock::now();
auto [i, j] = coio::this_thread::sync_wait(coio::when_all(
job(context.get_scheduler(), "foo", 114, 2s),
job(context.get_scheduler(), "bar", 514, 1s),
[](coio::time_loop& ctx) -> coio::task<> { // drive the loop as a third branch
ctx.run();
co_return;
}(context)
)).value();
const auto tock = std::chrono::steady_clock::now();
std::cout << "result: i = " << i << ", j = " << j << '\n'; // 114, 514
std::cout << "took: "
<< std::chrono::duration_cast<std::chrono::milliseconds>(tock - tick).count()
<< "ms\n"; // ~2000ms
}
The two jobs wait concurrently — total time is the maximum (2s), not the sum. The third when_all branch runs context.run() on the sync_wait thread, making it the loop's consumer; run() returns once both timers have fired and no work remains.
See also¶
- Execution contexts — the shared model in full
- work_guard — keep
run()alive without pending timers - timer — a scheduler-bound timer utility
- task