morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
coroutine.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <atomic>
5#include <chrono>
6#include <core/async/Awaitable.hpp>
7#include <core/async/Cancellation.hpp>
8#include <core/async/ExecutorContext.hpp>
9#include <core/async/IExecutor.hpp>
10#include <core/async/ParkedWork.hpp>
11#include <core/async/StopToken.hpp>
12#include <core/async/Task.hpp>
13#include <coroutine>
14#include <cstdint>
15#include <exception>
16#include <memory>
17#include <optional>
18#include <utility>
19
20#include "completion.hpp"
23#include "executor.hpp"
24#include "logger.hpp"
25#include "timeout_scheduler.hpp"
26
27/// @file
28/// @brief Coroutines on `core::async::Task`: awaiting a `Completion`, starting
29/// a detached flow on a morph executor, and waiting on a timer.
30///
31/// `Completion<T>::operator co_await` itself lives in `completion.hpp`, so a
32/// completion is awaitable wherever it is visible; this header adds `spawn` and
33/// `delay`, and brings in what drives a Task-returning model handler. Specified
34/// in `docs/spec/core/coroutines.md`.
35
36namespace morph::async {
37
38namespace detail {
39
40/// @brief Resumes coroutines through a `morph::exec::IExecutor`, as the
41/// current executor of each resumption.
42class ExecutorResumer final : public ::core::async::IExecutor, public std::enable_shared_from_this<ExecutorResumer> {
43public:
44 /// @param executor Where every resumption is posted; must outlive them all.
45 explicit ExecutorResumer(::morph::exec::IExecutor& executor MORPH_LIFETIMEBOUND) : _executor{executor} {}
46
47 using ::core::async::IExecutor::submit;
48
49 /// @brief Posts @p handle's resumption to the executor.
50 /// @param handle The coroutine to resume; borrowed.
51 void submit(std::coroutine_handle<> handle) override {
52 _executor.post([self = shared_from_this(), handle] {
53 std::shared_ptr<void> const keep = self;
54 ::core::async::ExecutorScope const scope{*self, &keep, nullptr};
55 handle.resume();
56 });
57 }
58
59 /// @brief Posts @p work's resumption to the executor.
60 /// @param work The coroutine to resume. The spawned task's driver owns its
61 /// frame, so the abandon claim is not taken.
62 void submit(::core::async::ParkedWork work) override { submit(work.resume); }
63
64private:
65 ::morph::exec::IExecutor& _executor;
66};
67
68/// @brief Suspends and resumes through @p target: how a spawned task's first
69/// step reaches its executor.
70struct HopTo {
71 ::core::async::IExecutor* target;
72
73 // Called through the awaiter by the compiler, so not static.
74 // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
75 [[nodiscard]] constexpr bool await_ready() const noexcept { return false; }
76 void await_suspend(std::coroutine_handle<> awaiting) const { target->submit(awaiting); }
77 void await_resume() const noexcept {}
78};
79
80/// @brief The detached coroutine `spawn` starts: hops onto the executor, runs
81/// the task there and logs what it lets escape.
82struct SpawnedTask {
83 // The compiler calls every member of a promise through the object, so none
84 // is made static.
85 // NOLINTBEGIN(readability-convert-member-functions-to-static)
86 struct promise_type {
87 [[nodiscard]] SpawnedTask get_return_object() const noexcept { return {}; }
88 [[nodiscard]] std::suspend_never initial_suspend() const noexcept { return {}; }
89 [[nodiscard]] std::suspend_never final_suspend() const noexcept { return {}; }
90 void return_void() const noexcept {}
91 // Only the hop can throw past the body's catch: an executor that cannot
92 // accept one closure, before anything has run.
93 [[noreturn]] void unhandled_exception() const noexcept { std::terminate(); }
94 };
95 // NOLINTEND(readability-convert-member-functions-to-static)
96};
97
98/// @brief The body of `spawn`.
99/// @param resumer The executor adapter; owned here for the task's lifetime.
100/// @param task The task to run.
101/// @return Nothing to hold: the coroutine frees itself when it finishes.
102inline SpawnedTask runSpawned(std::shared_ptr<ExecutorResumer> resumer, ::core::async::Task<void> task) {
103 co_await HopTo{resumer.get()};
104 try {
105 co_await std::move(task);
106 } catch (const std::exception& exc) {
107 ::morph::log::logError("[spawn] task threw: {}", exc.what());
108 } catch (...) {
109 ::morph::log::logError("[spawn] task threw unknown exception");
110 }
111}
112
113} // namespace detail
114
115/// @brief Starts @p task detached, with every resumption -- its first step
116/// included -- posted to @p executor.
117///
118/// The entry point from code that is not itself a coroutine: with a
119/// `QtExecutor`, a GUI flow written as a coroutine runs every step on the GUI
120/// thread. Awaits inside @p task resume on @p executor, whichever thread the
121/// awaited operation completed on. An exception @p task lets escape is logged
122/// through `morph::log` and swallowed. Its stop token is never stopped.
123/// @param executor Where the task runs; must outlive it.
124/// @param task The task to run.
125inline void spawn(::morph::exec::IExecutor& executor, ::core::async::Task<void> task) {
126 detail::runSpawned(std::make_shared<detail::ExecutorResumer>(executor), std::move(task));
127}
128
129/// @brief The awaiter `delay()` returns: suspends until a `TimeoutScheduler`
130/// entry fires, or until a stop withdraws it.
131///
132/// Resumes on the executor the awaiting coroutine was running on (core-cpp's
133/// current executor), or on the scheduler's loop thread if there was none.
134/// With a stoppable token on the awaiting promise, a stop cancels the scheduler
135/// entry -- its capture is released on the scheduler's loop -- and resumes the
136/// coroutine with
137/// `core::async::OperationCancelled` on the same executor, or, with none,
138/// inline on the thread that requested the stop. Exactly one of the timer and the stop resumes it.
140 enum class Outcome : std::uint8_t { Pending, Fired, Cancelled };
141
142 struct Shared;
143
144 struct OnStop {
145 Shared* shared;
146 void operator()() const noexcept { shared->onStop(); }
147 };
148
149 struct Shared : std::enable_shared_from_this<Shared> {
150 std::atomic<Outcome> outcome{Outcome::Pending};
151 std::atomic<bool> armed{false};
152 std::atomic<bool> claimed{false};
153 std::atomic<detail::TimeoutScheduler::Handle> timer{0};
154 std::coroutine_handle<> continuation;
155 ::core::async::ResumeTarget context;
156 detail::TimeoutScheduler* scheduler = nullptr;
157 std::optional<::core::async::StopCallback<OnStop>> stopCallback;
158
159 // seq_cst, as `armed` is: whoever decides and then reads `armed` must
160 // not miss the other side's store (a store-buffering pattern).
161 bool decide(Outcome outcome_) noexcept {
162 auto expected = Outcome::Pending;
163 return outcome.compare_exchange_strong(expected, outcome_, std::memory_order_seq_cst);
164 }
165
166 void resume() const {
167 if (context) {
168 context.submit(::core::async::ParkedWork{.resume = continuation});
169 } else {
170 continuation.resume();
171 }
172 }
173
174 void onStop() noexcept {
175 if (!decide(Outcome::Cancelled)) {
176 return;
177 }
178 try {
179 // The coroutine may be resumed below and free the awaiter;
180 // this keeps the state alive until the callback has returned.
181 auto const keep = shared_from_this();
182 scheduler->cancel(timer.load());
183 if (armed.load(std::memory_order_seq_cst) && !claimed.exchange(true, std::memory_order_acq_rel)) {
184 resume();
185 }
186 } catch (...) { // NOLINT(bugprone-empty-catch): a stop callback must not throw
187 // No executor to resume through: the coroutine stays
188 // suspended until its owner destroys it.
189 }
190 }
191 };
192
193public:
194 /// @param scheduler The scheduler whose entry times the wait; must outlive it.
195 /// @param duration How long to wait at least.
196 DelayAwaiter(detail::TimeoutScheduler& scheduler MORPH_LIFETIMEBOUND, std::chrono::milliseconds duration)
197 : _shared{std::make_shared<Shared>()}, _duration{duration} {
198 _shared->scheduler = &scheduler;
199 }
200
201 /// @brief Withdraws the wait if the frame is destroyed while suspended.
203 _shared->stopCallback.reset();
204 if (_shared->decide(Outcome::Cancelled)) {
205 _shared->scheduler->cancel(_shared->timer.load());
206 }
207 }
208
209 DelayAwaiter(const DelayAwaiter&) = delete;
210 DelayAwaiter& operator=(const DelayAwaiter&) = delete;
211 DelayAwaiter(DelayAwaiter&&) = delete;
212 DelayAwaiter& operator=(DelayAwaiter&&) = delete;
213
214 /// @return Always false: every decision is `await_suspend`'s.
215 // Called through the awaiter by the compiler, so not static.
216 // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
217 [[nodiscard]] constexpr bool await_ready() const noexcept { return false; }
218
219 /// @tparam Promise The awaiting coroutine's promise type.
220 /// @param awaiting The coroutine performing the `co_await`.
221 /// @return False to continue at once, when a stop was already requested;
222 /// true once the timer is armed.
223 template <typename Promise>
224 bool await_suspend(std::coroutine_handle<Promise> awaiting) {
225 // Copied before `armed` is published: from then on a stop may resume
226 // the coroutine and free this awaiter with its frame.
227 auto shared = _shared;
228 auto const duration = _duration;
229 shared->continuation = awaiting;
230 shared->context = ::core::async::ResumeTarget::current();
231 if constexpr (::core::async::HasStopToken<Promise>) {
232 ::core::async::StopToken const token = awaiting.promise().stopToken();
233 if (token.stop_requested()) {
234 shared->outcome.store(Outcome::Cancelled);
235 return false;
236 }
237 if (token.stop_possible()) {
238 shared->stopCallback.emplace(token, OnStop{shared.get()});
239 }
240 }
241 shared->armed.store(true, std::memory_order_seq_cst);
242 if (shared->outcome.load(std::memory_order_seq_cst) == Outcome::Cancelled) {
243 return shared->claimed.exchange(true, std::memory_order_acq_rel);
244 }
245 auto const handle = shared->scheduler->schedule(duration, [shared] {
246 if (shared->decide(Outcome::Fired)) {
247 shared->resume();
248 }
249 });
250 shared->timer.store(handle);
251 // A stop that won between arming and here found no timer to cancel.
252 if (shared->outcome.load() == Outcome::Cancelled) {
253 shared->scheduler->cancel(handle);
254 }
255 return true;
256 }
257
258 /// @throws core::async::OperationCancelled if a stop withdrew the wait.
260 _shared->stopCallback.reset();
261 if (_shared->outcome.load() == Outcome::Cancelled) {
262 throw ::core::async::OperationCancelled{};
263 }
264 }
265
266private:
267 std::shared_ptr<Shared> _shared;
268 std::chrono::milliseconds _duration;
269};
270
271/// @brief Suspends the awaiting coroutine for at least @p duration.
272///
273/// `co_await morph::async::delay(scheduler, 50ms)`. Stop-aware; see
274/// `DelayAwaiter`.
275/// @param scheduler The scheduler whose entry times the wait; must outlive it.
276/// @param duration How long to wait at least.
277/// @return The awaiter.
278[[nodiscard]] inline DelayAwaiter delay(detail::TimeoutScheduler& scheduler MORPH_LIFETIMEBOUND,
279 std::chrono::milliseconds duration) {
280 return DelayAwaiter{scheduler, duration};
281}
282
283} // namespace morph::async
constexpr Self self
The dialog or drawer a command list runs in.
Definition author.hpp:1295
The awaiter delay() returns: suspends until a TimeoutScheduler entry fires, or until a stop withdraws...
bool await_suspend(std::coroutine_handle< Promise > awaiting)
~DelayAwaiter()
Withdraws the wait if the frame is destroyed while suspended.
DelayAwaiter(detail::TimeoutScheduler &scheduler, std::chrono::milliseconds duration)
constexpr bool await_ready() const noexcept
The awaiter behind Completion<T>::operator co_await.
DelayAwaiter delay(detail::TimeoutScheduler &scheduler, std::chrono::milliseconds duration)
Suspends the awaiting coroutine for at least duration.
void spawn(::morph::exec::IExecutor &executor, ::core::async::Task< void > task)
Starts task detached, with every resumption – its first step included – posted to executor.
Abstract executor interface.
Definition executor.hpp:70
What drives a model's Task handler: the driver coroutine that starts and finishes it,...