Aria 3.2.6
C++23 MVVM framework — 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 <condition_variable>
8#include <coroutine>
9#include <cstdint>
10#include <deque>
11#include <functional>
12#include <future>
13#include <iterator>
14#include <memory>
15#include <mutex>
16#include <queue>
17#include <stdexcept>
18#include <thread>
19#include <vector>
20
21namespace aria::async {
22
37class IExecutor : public virtual aria::IScheduler {
38public:
39 ~IExecutor() override = default;
40
43 virtual void post(std::function<void()> fn) = 0;
44
45 // ── IScheduler bridge ────────────────────────────────────────────
51 void schedule(std::function<void()> fn) override {
52 post(std::move(fn));
53 }
54
55 // ── Legacy capability shims (kept for source compatibility) ──────
60 [[nodiscard]] virtual bool is_safe_graph_executor() const noexcept {
62 }
63
66 [[nodiscard]] virtual bool is_safe_worker_executor() const noexcept {
68 }
69};
70
73public:
74 explicit ThreadPoolExecutor(std::size_t threads = std::thread::hardware_concurrency())
75 : stop_(false) {
76 if (threads == 0) threads = 1;
77 try {
78 for (std::size_t i = 0; i < threads; ++i) {
79 workers_.emplace_back([this]() { worker_loop_(); });
80 }
81 } catch (...) {
82 // A failed thread creation must not destroy joinable std::threads
83 // (which terminates the process). Stop and join the started prefix.
84 {
85 std::scoped_lock lk(mutex_);
86 stop_ = true;
87 }
88 wake_sequence_.fetch_add(1, std::memory_order_release);
89 wake_sequence_.notify_all();
90 for (auto& worker : workers_) {
91 if (worker.joinable()) { worker.join(); }
92 }
93 throw;
94 }
95 }
96
98 // Drain: wait until the queue is empty AND no worker is running a
99 // task. Detached coroutines that hop off to another executor will
100 // re-post back to us when they resume; each post increments
101 // `inflight_`, so as long as we wait on `inflight_ == 0` we won't
102 // tear down the threadpool out from under a pending resume.
103 //
104 // CONTRACT: callers MUST ensure no new work is posted to this pool
105 // after they begin destroying it. The standard pattern is:
106 // 1. Cancel every CoroutineScope that launches work on this pool.
107 // 2. Allow cancelled coroutines to throw/unwind (they may still
108 // post one final resume — `wait_idle` will wait for it).
109 // 3. Destroy the pool.
110 wait_idle();
111 {
112 std::lock_guard lk(mutex_);
113 stop_ = true;
114 }
115 wake_sequence_.fetch_add(1, std::memory_order_release);
116 wake_sequence_.notify_all();
117 for (auto& t : workers_) if (t.joinable()) t.join();
118 }
119
120 void post(std::function<void()> fn) override {
121 {
122 std::lock_guard lk(mutex_);
123 queue_.push(std::move(fn));
124 inflight_.fetch_add(1, std::memory_order_relaxed);
125 }
126 wake_sequence_.fetch_add(1, std::memory_order_release);
127 wake_sequence_.notify_one();
128 }
129
134 [[nodiscard]] aria::SchedulerCaps caps() const noexcept override {
138 }
139
140 [[nodiscard]] std::size_t worker_count() const noexcept { return workers_.size(); }
141
144 void wait_idle() {
145 while (true) {
146 int pending = 0;
147 {
148 // Queue publication and its count increment are one locked
149 // operation. Sample both before waiting outside the lock.
150 std::lock_guard lk(mutex_);
151 pending = inflight_.load(std::memory_order_acquire);
152 if (queue_.empty() && pending == 0) { return; }
153 }
154 // Completion between unlock and wait changes the sampled value,
155 // so a notification cannot be lost in that handoff window.
156 inflight_.wait(pending, std::memory_order_acquire);
157 }
158 }
159
160private:
161 void worker_loop_() {
162 while (true) {
163 std::function<void()> fn;
164 {
165 std::unique_lock lk(mutex_);
166 while (!stop_ && queue_.empty()) {
167 const auto sequence = wake_sequence_.load(std::memory_order_acquire);
168 lk.unlock();
169 wake_sequence_.wait(sequence, std::memory_order_acquire);
170 lk.lock();
171 }
172 if (stop_ && queue_.empty()) return;
173 fn = std::move(queue_.front());
174 queue_.pop();
175 }
176 try {
177 fn();
178 } catch (...) {
180 std::string_view{"executor.thread_pool.worker"},
181 std::current_exception());
182 }
183 // Decrement AFTER the task has finished (not when we dequeued).
184 if (inflight_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
185 // Atomic waiters need no queue-lock handshake on completion.
186 inflight_.notify_all();
187 }
188 }
189 }
190
191 std::vector<std::thread> workers_;
192 std::queue<std::function<void()>> queue_;
193 std::mutex mutex_;
194 // Publication epochs avoid the condition-variable re-lock handshake.
195 // A worker samples the epoch while holding the queue lock, then waits
196 // outside it: a post between unlock and wait changes the sampled value.
197 std::atomic<std::uint64_t> wake_sequence_{0};
198 // Reserved to retain the existing ThreadPoolExecutor object layout.
199 [[maybe_unused]] std::condition_variable idle_cv_;
200 std::atomic<int> inflight_{0};
201 bool stop_;
202};
203
218class InlineExecutor : public IExecutor {
219public:
224 void post(std::function<void()> fn) override {
225 if (!fn) return;
226 try {
227 fn();
228 } catch (...) {
230 std::string_view{"executor.inline.post"},
231 std::current_exception());
232 }
233 }
234
235 // Inline runs synchronously on the caller's thread, so it cannot
236 // claim main-thread affinity, pumping, or autonomy. The specific
237 // "Inline graph + non-Inline worker" race is rejected independently
238 // by an explicit dynamic_cast in detail::check_executor_safety_runtime
239 // — see async_command.hpp.
240 [[nodiscard]] aria::SchedulerCaps caps() const noexcept override {
244 }
245};
246
282public:
288
290 std::deque<std::function<void()>> retired;
291 {
292 std::scoped_lock lk(m_);
293 closed_ = true;
294 retired.swap(queue_);
295 }
296 // Reject work posted by capture destructors while all members are live.
297 }
298
299 void post(std::function<void()> fn) override {
300 if (!fn) { return; }
301 {
302 std::lock_guard lk(m_);
303 if (closed_) { return; }
304 queue_.push_back(std::move(fn));
305 }
306 // An already-bound owner cannot simultaneously be asleep in its own
307 // pump. Foreign producers and an unbound executor must still wake it.
308 if (owner_.load(std::memory_order_acquire) != std::this_thread::get_id()) {
309 cv_.notify_one();
310 }
311 }
312
323
324 [[nodiscard]] bool is_main_thread() const noexcept override {
325 return is_owner_thread();
326 }
327
334 std::size_t drain() {
335 bind_owner_();
336 std::size_t total = 0;
337 while (true) {
338 std::function<void()> single;
339 std::vector<std::function<void()>> local;
340 {
341 std::lock_guard lk(m_);
342 if (queue_.empty()) { break; }
343 if (queue_.size() == 1) {
344 single = std::move(queue_.front());
345 queue_.pop_front();
346 } else {
347 local.reserve(queue_.size());
348 std::move(queue_.begin(), queue_.end(), std::back_inserter(local));
349 queue_.clear();
350 }
351 }
352 // The common coroutine handoff carries one callable. Keep it on
353 // the stack rather than allocating a vector for every resume.
354 // Multi-callable batches still detach together before callbacks.
355 if (single) {
356 invoke_drained(single);
357 ++total;
358 } else {
359 for (auto& fn : local) {
360 invoke_drained(fn);
361 ++total;
362 }
363 }
364 }
365 return total;
366 }
367
374 template<typename Pred>
375 bool pump_until(Pred predicate,
376 std::chrono::milliseconds timeout = std::chrono::seconds{2}) {
377 bind_owner_();
378 const auto deadline = std::chrono::steady_clock::now() + timeout;
379 while (true) {
380 drain();
381 if (predicate()) return true;
382 const auto now = std::chrono::steady_clock::now();
383 if (now >= deadline) return false;
384 std::unique_lock lk(m_);
385 cv_.wait_until(lk, deadline, [this]{ return !queue_.empty(); });
386 // Loop: if predicate is already true the next drain is a no-op
387 // and we return true. If we woke on timeout the next iteration
388 // will see `now >= deadline` and bail.
389 }
390 }
391
396 void run_one() {
397 bind_owner_();
398 std::function<void()> fn;
399 {
400 std::unique_lock lk(m_);
401 cv_.wait(lk, [this]{ return !queue_.empty(); });
402 fn = std::move(queue_.front());
403 queue_.pop_front();
404 }
405 try {
406 fn();
407 } catch (...) {
409 std::string_view{"executor.main_thread.run_one"},
410 std::current_exception());
411 }
412 }
413
416 [[nodiscard]] bool is_owner_thread() const noexcept {
417 const auto id = owner_.load(std::memory_order_acquire);
418 return id == std::thread::id{} || id == std::this_thread::get_id();
419 }
420
421 [[nodiscard]] std::size_t pending() const noexcept {
422 std::lock_guard lk(m_);
423 return queue_.size();
424 }
425
427 void clear() {
428 bind_owner_();
429 std::deque<std::function<void()>> retired;
430 {
431 std::lock_guard lk(m_);
432 retired.swap(queue_);
433 }
434 // Destroy user captures after unlocking: their destructors may post.
435
436 }
437
438private:
439 static void invoke_drained(std::function<void()>& fn) {
440 try {
441 fn();
442 } catch (...) {
444 std::string_view{"executor.main_thread.drain"},
445 std::current_exception());
446 }
447 }
448
449 void bind_owner_() {
450 const auto self = std::this_thread::get_id();
451 if (owner_.load(std::memory_order_acquire) == self) { return; }
452 std::thread::id expected{};
453 if (owner_.compare_exchange_strong(expected, self,
454 std::memory_order_acq_rel)) {
455 return; // we just claimed ownership
456 }
457 // Already bound — must match.
458 if (expected != self) {
459 throw std::logic_error(
460 "MainThreadExecutor must be pumped or cleared on its owner thread");
461 }
462 }
463
464 mutable std::mutex m_;
465 std::condition_variable cv_;
466 std::deque<std::function<void()>> queue_;
467 std::atomic<std::thread::id> owner_{};
468 bool closed_ = false;
469};
470
472inline auto schedule_on(IExecutor& exec) {
473 struct Awaiter {
474 IExecutor& exec;
475 bool await_ready() const noexcept { return false; }
476 void await_suspend(std::coroutine_handle<> h) const {
477 exec.post([h]() mutable { h.resume(); });
478 }
479 void await_resume() const noexcept {}
480 };
481 return Awaiter{exec};
482}
483
484} // namespace aria::async
Definition scheduler.hpp:152
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:37
~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:60
void schedule(std::function< void()> fn) override
Submit fn for execution "soon". Defines Caps::Post.
Definition executor.hpp:51
virtual void post(std::function< void()> fn)=0
Legacy / canonical executor entry point.
aria::SchedulerCaps caps() const noexcept override
Capability bitmask.
Definition executor.hpp:46
virtual bool is_safe_worker_executor() const noexcept
True iff this executor can host worker tasks.
Definition executor.hpp:66
Inline executor — runs callable synchronously on the calling thread.
Definition executor.hpp:218
void post(std::function< void()> fn) override
Synchronously runs fn on the caller's thread.
Definition executor.hpp:224
aria::SchedulerCaps caps() const noexcept override
Capability bitmask.
Definition executor.hpp:240
MainThreadExecutor & operator=(const MainThreadExecutor &)=delete
void clear()
Drop all pending callables without running them. Owner-thread-only.
Definition executor.hpp:427
~MainThreadExecutor() override
Definition executor.hpp:289
void post(std::function< void()> fn) override
Legacy / canonical executor entry point.
Definition executor.hpp:299
MainThreadExecutor & operator=(MainThreadExecutor &&)=delete
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:416
std::size_t pending() const noexcept
Definition executor.hpp:421
void run_one()
Run exactly one callable, blocking the owner thread until one is available.
Definition executor.hpp:396
MainThreadExecutor(MainThreadExecutor &&)=delete
std::size_t drain()
Run callables.
Definition executor.hpp:334
MainThreadExecutor(const MainThreadExecutor &)=delete
bool is_main_thread() const noexcept override
True iff the calling thread is the scheduler's "main" thread.
Definition executor.hpp:324
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:316
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:375
aria::SchedulerCaps caps() const noexcept override
Worker pool: NOT safe as the graph executor.
Definition executor.hpp:134
~ThreadPoolExecutor() override
Definition executor.hpp:97
void wait_idle()
Block until queue is drained AND no worker is currently running a task.
Definition executor.hpp:144
std::size_t worker_count() const noexcept
Definition executor.hpp:140
void post(std::function< void()> fn) override
Legacy / canonical executor entry point.
Definition executor.hpp:120
ThreadPoolExecutor(std::size_t threads=std::thread::hardware_concurrency())
Definition executor.hpp:74
Definition async_command.hpp:118
auto schedule_on(IExecutor &exec)
Schedule a coroutine to resume on the given executor.
Definition executor.hpp:472
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