morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
task_handler.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <atomic>
5#include <concepts>
6#include <core/async/StopToken.hpp>
7#include <core/async/Task.hpp>
8#include <coroutine>
9#include <cstddef>
10#include <deque>
11#include <exception>
12#include <functional>
13#include <memory>
14#include <optional>
15#include <thread>
16#include <utility>
17
18#include "../strand.hpp"
19
20/// @file
21/// @brief What drives a model's Task handler: the driver coroutine that starts
22/// and finishes it, the per-instance gate that keeps actions from
23/// overlapping, and the trait that tells a Task handler from an ordinary
24/// one. The resumer its resumptions go through is `strand.hpp`'s.
25///
26/// Specified in `docs/spec/core/coroutines.md`, "Model side".
27
28namespace morph::model {
29
30/// @brief The result an action handler's return type stands for: `R` for a
31/// `core::async::Task<R>` handler, the type itself for any other.
32/// @tparam T The handler's return type.
33template <typename T>
35 /// @brief The action's result type.
36 using type = T;
37 /// @brief Whether the handler is a coroutine returning `core::async::Task`.
38 static constexpr bool isTask = false;
39};
40
41/// @brief `HandlerResult` for a Task handler.
42/// @tparam R The value the Task produces.
43template <typename R>
44struct HandlerResult<::core::async::Task<R>> {
45 /// @brief The action's result type: what the Task produces.
46 using type = R;
47 /// @brief Whether the handler is a coroutine returning `core::async::Task`.
48 static constexpr bool isTask = true;
49};
50
51/// @brief The action result a handler returning @p T produces.
52/// @tparam T The handler's return type.
53template <typename T>
55
56/// @brief Whether a handler returning @p T is a Task handler.
57/// @tparam T The handler's return type.
58template <typename T>
60
61namespace detail {
62
63/// @brief Keeps a model instance's actions from overlapping, across a Task
64/// handler's suspensions.
65///
66/// A strand serialises the tasks posted to it, and a suspended Task handler is
67/// not one: while it waits the strand is free. Every action therefore enters
68/// this gate on the strand before it runs and leaves it when it is finished --
69/// for a Task handler, when the Task completes. An action that finds the gate
70/// held is queued, in arrival order, and started by the `leave()` that frees
71/// it. Touched only on the model's strand, so it takes no lock.
72class ActionGate {
73public:
74 /// @brief Starts @p start now, or queues it behind the action holding the gate.
75 ///
76 /// The started action holds the gate until it calls `leave()`.
77 ///
78 /// A template rather than a `std::function` parameter: an action that
79 /// starts at once is called as it is, and only one that has to wait is
80 /// type-erased into the queue. libstdc++'s `std::function` heap-allocates
81 /// any callable that is not trivially copyable, a lambda holding a
82 /// `shared_ptr` included, and an action on a free gate need not pay that.
83 /// @param start The action; must not throw. Copyable, as the queue's
84 /// `std::function` requires.
85 template <std::invocable Start>
86 void enter(Start&& start) {
87 Occupancy const occupancy{*this};
88 if (_held || (_waiting != nullptr && !_waiting->empty())) {
89 if (_waiting == nullptr) {
90 _waiting = std::make_unique<std::deque<std::function<void()>>>();
91 }
92 _waiting->emplace_back(std::forward<Start>(start));
93 return;
94 }
95 _held = true;
96 start();
97 }
98
99 /// @brief Takes the gate if it is free, for an action its caller then runs
100 /// itself; otherwise leaves it to `enter` to queue the action.
101 ///
102 /// For a caller whose action stays owned where it is while it runs: a
103 /// backend's strand task, which holds an ordinary run by value, so an
104 /// action on a free gate costs no allocation beyond the post.
105 /// @return True if taken: the caller runs its action now, and the action
106 /// holds the gate until it calls `leave()`. False if the action has
107 /// to wait, through `enter`.
108 [[nodiscard]] bool tryEnter() {
109 Occupancy const occupancy{*this};
110 if (_held || (_waiting != nullptr && !_waiting->empty())) {
111 return false;
112 }
113 _held = true;
114 return true;
115 }
116
117 /// @brief Releases the gate and starts the actions queued behind it, oldest
118 /// first, for as long as each one finishes without suspending.
119 ///
120 /// A loop rather than a recursion: a queue of ordinary handlers would
121 /// otherwise nest one stack frame per queued action.
122 void leave() {
123 Occupancy const occupancy{*this};
124 _held = false;
125 if (_draining) {
126 return;
127 }
128 _draining = true;
129 while (!_held && _waiting != nullptr && !_waiting->empty()) {
130 auto next = std::move(_waiting->front());
131 _waiting->pop_front();
132 _held = true;
133 next();
134 }
135 _draining = false;
136 }
137
138 /// @brief How many times, process-wide, two threads have been inside
139 /// `enter` or `leave` of one gate at once.
140 ///
141 /// Always zero when the gate is used as specified: a gate is touched only
142 /// on its model's strand. A diagnostic for tests, which assert that it
143 /// does not change; the check that counts is one atomic compare-exchange
144 /// per call.
145 /// @return The count so far.
146 [[nodiscard]] static std::size_t overlapsObserved() noexcept { return overlapCounter().load(); }
147
148private:
149 static std::atomic<std::size_t>& overlapCounter() noexcept {
150 static std::atomic<std::size_t> counter{0};
151 return counter;
152 }
153
154 /// Marks the calling thread as inside the gate for the scope's lifetime,
155 /// and counts it if another thread already is. Reentry on the same thread,
156 /// which `leave()` starting the next action does, is not an overlap.
157 class Occupancy {
158 public:
159 explicit Occupancy(ActionGate& gate) noexcept : _gate{&gate} {
160 auto expected = std::thread::id{};
161 auto const self = std::this_thread::get_id();
162 _owner = _gate->_occupant.compare_exchange_strong(expected, self);
163 if (!_owner && expected != self) {
164 overlapCounter().fetch_add(1);
165 }
166 }
167 Occupancy(const Occupancy&) = delete;
168 Occupancy& operator=(const Occupancy&) = delete;
169 Occupancy(Occupancy&&) = delete;
170 Occupancy& operator=(Occupancy&&) = delete;
171 ~Occupancy() {
172 if (_owner) {
173 _gate->_occupant.store(std::thread::id{});
174 }
175 }
176
177 private:
178 ActionGate* _gate;
179 bool _owner = false;
180 };
181
182 std::atomic<std::thread::id> _occupant;
183 bool _held = false;
184 bool _draining = false;
185 /// Allocated the first time an action has to wait, which only a suspended
186 /// Task handler causes: every model instance carries a gate, and one that
187 /// never queues should cost a model holder nothing but a pointer.
188 std::unique_ptr<std::deque<std::function<void()>>> _waiting;
189};
190
191/// @brief The coroutine that runs one Task handler to completion.
192///
193/// Created suspended so its promise's stop token can be set before the first
194/// step; `startTaskHandler` resumes it. It frees itself at the end
195/// (`final_suspend` never suspends). Its promise answers `stopToken()`, which is
196/// how the token reaches the handler: `Task`'s awaiter copies the awaiting
197/// promise's token into the handler's.
198struct TaskHandlerDriver {
199 // The compiler calls every member of a promise through the object, so none
200 // is made static.
201 // NOLINTBEGIN(readability-convert-member-functions-to-static)
202 struct promise_type {
203 ::core::async::StopToken token;
204
205 TaskHandlerDriver get_return_object() noexcept {
206 return TaskHandlerDriver{std::coroutine_handle<promise_type>::from_promise(*this)};
207 }
208 [[nodiscard]] std::suspend_always initial_suspend() const noexcept { return {}; }
209 [[nodiscard]] std::suspend_never final_suspend() const noexcept { return {}; }
210 void return_void() const noexcept {}
211 // Every exception is caught in the driver's body, which settles the call
212 // with it; one escaping here would mean the settle itself threw.
213 [[noreturn]] void unhandled_exception() const noexcept { std::terminate(); }
214 [[nodiscard]] ::core::async::StopToken stopToken() const noexcept { return token; }
215 };
216 // NOLINTEND(readability-convert-member-functions-to-static)
217
218 std::coroutine_handle<promise_type> handle;
219};
220
221/// @brief The driver body: awaits @p task and hands its outcome to @p done.
222/// @tparam R The handler's result type.
223/// @param executor The handler's resumer. Held in the frame, so it lives until
224/// the handler has finished whatever holds it besides.
225/// @param task The handler's Task, not yet started.
226/// @param done Called once, on the strand, with the result or the exception.
227/// @return The suspended driver.
228template <typename R>
229TaskHandlerDriver driveTaskHandler(std::shared_ptr<::morph::exec::detail::TaskResumer> executor,
230 ::core::async::Task<R> task,
231 std::function<void(std::optional<R>, std::exception_ptr)> done) {
232 static_cast<void>(executor);
233 std::optional<R> result;
234 std::exception_ptr error;
235 try {
236 // The await is its own statement: MSVC cannot tail-call a call that
237 // shares a full-expression with a `co_await` (C4737).
238 R value = co_await std::move(task);
239 result.emplace(std::move(value));
240 } catch (...) {
241 error = std::current_exception();
242 }
243 done(std::move(result), error);
244}
245
246/// @brief Starts a Task handler on the model's strand, in the calling strand task.
247///
248/// The handler's first step runs here, inside @p executor -- the current
249/// executor, with the action's session -- and with the stop token set; every
250/// later step is resumed through @p executor, on the same strand.
251/// @tparam R The handler's result type.
252/// @param executor The handler's resumer.
253/// @param task The handler's Task, as the handler call returned it.
254/// @param token The stop token the handler observes.
255/// @param done Called once, on the strand, with the result or the exception.
256template <typename R>
257void startTaskHandler(const std::shared_ptr<::morph::exec::detail::TaskResumer>& executor, ::core::async::Task<R> task,
258 ::core::async::StopToken token, std::function<void(std::optional<R>, std::exception_ptr)> done) {
259 auto driver = driveTaskHandler<R>(executor, std::move(task), std::move(done));
260 driver.handle.promise().token = std::move(token);
261 executor->resumeHere(driver.handle);
262}
263
264} // namespace detail
265
266} // namespace morph::model
morph's strands: one per model instance, from core-cpp's core::async::KeyedStrands,...
R type
The action's result type: what the Task produces.
static constexpr bool isTask
Whether the handler is a coroutine returning core::async::Task.
The result an action handler's return type stands for: R for a core::async::Task<R> handler,...
T type
The action's result type.
static constexpr bool isTask
HandlerResult< T >::type HandlerResultT
The action result a handler returning T produces.
constexpr bool isTaskHandler
Whether a handler returning T is a Task handler.