Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
executor.hpp
Go to the documentation of this file.
1#pragma once
2
4#include "aria/scheduler.hpp"
5
6#include <atomic>
7#include <cassert>
8#include <condition_variable>
9#include <coroutine>
10#include <deque>
11#include <functional>
12#include <future>
13#include <iterator>
14#include <memory>
15#include <mutex>
16#include <queue>
17#include <thread>
18#include <vector>
19
20namespace aria::async {
21
36class IExecutor : public virtual aria::IScheduler {
37public:
38 ~IExecutor() override = default;
39
42 virtual void post(std::function<void()> fn) = 0;
43
44 // ── IScheduler bridge ────────────────────────────────────────────
50 void schedule(std::function<void()> fn) override {
51 post(std::move(fn));
52 }
53
54 // ── Legacy capability shims (kept for source compatibility) ──────
59 [[nodiscard]] virtual bool is_safe_graph_executor() const noexcept {
61 }
62
65 [[nodiscard]] virtual bool is_safe_worker_executor() const noexcept {
67 }
68};
69
72public:
73 explicit ThreadPoolExecutor(std::size_t threads = std::thread::hardware_concurrency())
74 : stop_(false) {
75 if (threads == 0) threads = 1;
76 for (std::size_t i = 0; i < threads; ++i) {
77 workers_.emplace_back([this]() { worker_loop_(); });
78 }
79 }
80
82 // Drain: wait until the queue is empty AND no worker is running a
83 // task. Detached coroutines that hop off to another executor will
84 // re-post back to us when they resume; each post increments
85 // `inflight_`, so as long as we wait on `inflight_ == 0` we won't
86 // tear down the threadpool out from under a pending resume.
87 //
88 // CONTRACT: callers MUST ensure no new work is posted to this pool
89 // after they begin destroying it. The standard pattern is:
90 // 1. Cancel every CoroutineScope that launches work on this pool.
91 // 2. Allow cancelled coroutines to throw/unwind (they may still
92 // post one final resume — `wait_idle` will wait for it).
93 // 3. Destroy the pool.
94 wait_idle();
95 {
96 std::lock_guard lk(mutex_);
97 stop_ = true;
98 }
99 cv_.notify_all();
100 for (auto& t : workers_) if (t.joinable()) t.join();
101 }
102
103 void post(std::function<void()> fn) override {
104 {
105 std::lock_guard lk(mutex_);
106 queue_.push(std::move(fn));
107 inflight_.fetch_add(1, std::memory_order_relaxed);
108 }
109 cv_.notify_one();
110 }
111
116 [[nodiscard]] aria::SchedulerCaps caps() const noexcept override {
120 }
121
122 [[nodiscard]] std::size_t worker_count() const noexcept { return workers_.size(); }
123
127 void wait_idle() {
128 std::unique_lock lk(mutex_);
129 idle_cv_.wait(lk, [this] {
130 return queue_.empty() && inflight_.load() == 0;
131 });
132 }
133
134private:
135 void worker_loop_() {
136 while (true) {
137 std::function<void()> fn;
138 {
139 std::unique_lock lk(mutex_);
140 cv_.wait(lk, [this] { return stop_ || !queue_.empty(); });
141 if (stop_ && queue_.empty()) return;
142 fn = std::move(queue_.front());
143 queue_.pop();
144 }
145 try {
146 fn();
147 } catch (...) {
149 std::string_view{"executor.thread_pool.worker"},
150 std::current_exception());
151 }
152 // Decrement AFTER the task has finished (not when we dequeued).
153 if (inflight_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
154 // Last one — notify a possibly-waiting destructor.
155 std::lock_guard lk(mutex_);
156 idle_cv_.notify_all();
157 }
158 }
159 }
160
161 std::vector<std::thread> workers_;
162 std::queue<std::function<void()>> queue_;
163 std::mutex mutex_;
164 std::condition_variable cv_; // wakes workers
165 std::condition_variable idle_cv_; // wakes wait_idle()
166 std::atomic<int> inflight_{0};
167 bool stop_;
168};
169
184class InlineExecutor : public IExecutor {
185public:
190 void post(std::function<void()> fn) override {
191 if (!fn) return;
192 try {
193 fn();
194 } catch (...) {
196 std::string_view{"executor.inline.post"},
197 std::current_exception());
198 }
199 }
200
201 // Inline runs synchronously on the caller's thread, so it cannot
202 // claim main-thread affinity, pumping, or autonomy. The specific
203 // "Inline graph + non-Inline worker" race is rejected independently
204 // by an explicit dynamic_cast in detail::check_executor_safety_runtime
205 // — see async_command.hpp.
206 [[nodiscard]] aria::SchedulerCaps caps() const noexcept override {
210 }
211};
212
248public:
249 void post(std::function<void()> fn) override {
250 {
251 std::lock_guard lk(m_);
252 queue_.push_back(std::move(fn));
253 }
254 cv_.notify_one();
255 }
256
267
268 [[nodiscard]] bool is_main_thread() const noexcept override {
269 return is_owner_thread();
270 }
271
278 std::size_t drain() {
279 bind_owner_();
280 std::size_t total = 0;
281 while (true) {
282 std::vector<std::function<void()>> local;
283 {
284 std::lock_guard lk(m_);
285 if (queue_.empty()) break;
286 local.reserve(queue_.size());
287 std::move(queue_.begin(), queue_.end(), std::back_inserter(local));
288 queue_.clear();
289 }
290 for (auto& fn : local) {
291 try {
292 fn();
293 } catch (...) {
295 std::string_view{"executor.main_thread.drain"},
296 std::current_exception());
297 }
298 ++total;
299 }
300 }
301 return total;
302 }
303
310 template<typename Pred>
311 bool pump_until(Pred predicate,
312 std::chrono::milliseconds timeout = std::chrono::seconds{2}) {
313 bind_owner_();
314 const auto deadline = std::chrono::steady_clock::now() + timeout;
315 while (true) {
316 drain();
317 if (predicate()) return true;
318 const auto now = std::chrono::steady_clock::now();
319 if (now >= deadline) return false;
320 std::unique_lock lk(m_);
321 cv_.wait_until(lk, deadline, [this]{ return !queue_.empty(); });
322 // Loop: if predicate is already true the next drain is a no-op
323 // and we return true. If we woke on timeout the next iteration
324 // will see `now >= deadline` and bail.
325 }
326 }
327
332 void run_one() {
333 bind_owner_();
334 std::function<void()> fn;
335 {
336 std::unique_lock lk(m_);
337 cv_.wait(lk, [this]{ return !queue_.empty(); });
338 fn = std::move(queue_.front());
339 queue_.pop_front();
340 }
341 try {
342 fn();
343 } catch (...) {
345 std::string_view{"executor.main_thread.run_one"},
346 std::current_exception());
347 }
348 }
349
352 [[nodiscard]] bool is_owner_thread() const noexcept {
353 const auto id = owner_.load(std::memory_order_acquire);
354 return id == std::thread::id{} || id == std::this_thread::get_id();
355 }
356
357 [[nodiscard]] std::size_t pending() const noexcept {
358 std::lock_guard lk(m_);
359 return queue_.size();
360 }
361
363 void clear() noexcept {
364 bind_owner_();
365 std::deque<std::function<void()>> retired;
366 {
367 std::lock_guard lk(m_);
368 retired.swap(queue_);
369 }
370 // Destroy user captures after unlocking: their destructors may post.
371
372 }
373
374private:
375 void bind_owner_() noexcept {
376 std::thread::id expected{};
377 const auto self = std::this_thread::get_id();
378 if (owner_.compare_exchange_strong(expected, self,
379 std::memory_order_acq_rel)) {
380 return; // we just claimed ownership
381 }
382 // Already bound — must match.
383 assert(expected == self
384 && "MainThreadExecutor pumped from a non-owner thread. "
385 "post() is fine from any thread, but drain/pump/run_one "
386 "must run on the thread that originally bound the executor.");
387 }
388
389 mutable std::mutex m_;
390 std::condition_variable cv_;
391 std::deque<std::function<void()>> queue_;
392 std::atomic<std::thread::id> owner_{};
393};
394
396inline auto schedule_on(IExecutor& exec) {
397 struct Awaiter {
398 IExecutor& exec;
399 bool await_ready() const noexcept { return false; }
400 void await_suspend(std::coroutine_handle<> h) const {
401 exec.post([h]() mutable { h.resume(); });
402 }
403 void await_resume() const noexcept {}
404 };
405 return Awaiter{exec};
406}
407
408} // namespace aria::async
Definition scheduler.hpp:152
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:36
~IExecutor() override=default
virtual bool is_safe_graph_executor() const noexcept
True iff this executor is safe to use as the graph-thread (UI) executor.
Definition executor.hpp:59
void schedule(std::function< void()> fn) override
Submit fn for execution "soon". Defines Caps::Post.
Definition executor.hpp:50
virtual void post(std::function< void()> fn)=0
Legacy / canonical executor entry point.
aria::SchedulerCaps caps() const noexcept override
Capability bitmask.
Definition executor.hpp:45
virtual bool is_safe_worker_executor() const noexcept
True iff this executor can host worker tasks.
Definition executor.hpp:65
Inline executor — runs callable synchronously on the calling thread.
Definition executor.hpp:184
void post(std::function< void()> fn) override
Synchronously runs fn on the caller's thread.
Definition executor.hpp:190
aria::SchedulerCaps caps() const noexcept override
Capability bitmask.
Definition executor.hpp:206
Main-thread executor — queues callables for later execution on the thread that "owns" the executor (t...
Definition executor.hpp:247
void clear() noexcept
Drop all pending callables without running them. Owner-thread-only.
Definition executor.hpp:363
void post(std::function< void()> fn) override
Legacy / canonical executor entry point.
Definition executor.hpp:249
bool is_owner_thread() const noexcept
True if called from the thread that owns this executor (or if no owner has been bound yet).
Definition executor.hpp:352
std::size_t pending() const noexcept
Definition executor.hpp:357
void run_one()
Run exactly one callable, blocking the owner thread until one is available.
Definition executor.hpp:332
std::size_t drain()
Run callables.
Definition executor.hpp:278
bool is_main_thread() const noexcept override
True iff the calling thread is the scheduler's "main" thread.
Definition executor.hpp:268
aria::SchedulerCaps caps() const noexcept override
Main-thread executor: safe in both reactive roles, plus Pumpable (drain/pump_until/run_one) and MainT...
Definition executor.hpp:260
bool pump_until(Pred predicate, std::chrono::milliseconds timeout=std::chrono::seconds{2})
Pump until predicate() returns true OR timeout elapses, then return.
Definition executor.hpp:311
aria::SchedulerCaps caps() const noexcept override
Worker pool: NOT safe as the graph executor.
Definition executor.hpp:116
~ThreadPoolExecutor() override
Definition executor.hpp:81
void wait_idle()
Block until queue is drained AND no worker is currently running a task.
Definition executor.hpp:127
std::size_t worker_count() const noexcept
Definition executor.hpp:122
void post(std::function< void()> fn) override
Legacy / canonical executor entry point.
Definition executor.hpp:103
ThreadPoolExecutor(std::size_t threads=std::thread::hardware_concurrency())
Definition executor.hpp:73
Definition async_command.hpp:118
auto schedule_on(IExecutor &exec)
Schedule a coroutine to resume on the given executor.
Definition executor.hpp:396
bool has_caps(const IScheduler &s, SchedulerCaps required) noexcept
Definition scheduler.hpp:184
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
SchedulerCaps
Definition scheduler.hpp:83
@ Post
Can submit "fire now" work.
Definition scheduler.hpp:89
@ GraphSafe
Safe to use as the graph-thread executor — i.e.
Definition scheduler.hpp:107
@ Pumpable
Posted work is held in a queue until a pump-style call drains it (e.g.
Definition scheduler.hpp:102
@ Autonomous
Implementation does not require any external pump and runs work on background threads autonomously.
Definition scheduler.hpp:116
@ MainThread
Submitted work runs on a single, identifiable "main" thread that is consistent across calls.
Definition scheduler.hpp:97
@ WorkerSafe
Safe to host blocking worker tasks (e.g.
Definition scheduler.hpp:111