morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
io_loop.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cassert>
5#include <core/async/ExecutorContext.hpp>
6#include <core/net/EventLoop.hpp>
7#include <core/net/PlatformLoop.hpp>
8#include <cstdint>
9#include <exception>
10#include <future>
11#include <memory>
12#include <type_traits>
13#include <utility>
14
15#include "executor.hpp"
16#include "logger.hpp"
17#include "profiler.hpp"
18
19#if defined(__EMSCRIPTEN__) && !defined(__EMSCRIPTEN_PTHREADS__)
20#define MORPH_IO_LOOP_HOST_DRIVEN 1
21#else
22#include <thread>
23#endif
24
25/// @file
26/// `IoLoop` — the one I/O loop an application runs, and the owner of every
27/// socket, timer and probe built on it.
28///
29/// `morph::net::SocketBackend`, `morph::net::SocketServer`,
30/// `TimeoutScheduler` and `offline::NetworkMonitor` own no thread. Each keeps
31/// its state on a `core::net::PlatformLoop` and touches it only in tasks that
32/// loop runs; its public verbs post to the loop and return. An application
33/// constructs one `IoLoop` and hands it to every one of them, so one thread
34/// carries every connection, every deadline and the connectivity probe.
35///
36/// @par Who turns the loop
37/// - **`IoLoopDriver::OwnThread`** (native default): the `IoLoop` owns one
38/// thread that runs the loop until the `IoLoop` is destroyed.
39/// - **`IoLoopDriver::Caller`**: no thread is started. The constructing thread
40/// is the loop's and turns it — `loop().runOnce()`, `loop().runUntilIdle()`
41/// or a `blockOn()` — so a terminal UI and its sockets, timers and bridge
42/// callbacks share that one thread. No other thread may turn it.
43/// - **Single-threaded WebAssembly** (`__EMSCRIPTEN__` without
44/// `__EMSCRIPTEN_PTHREADS__`): there is no thread to start whichever driver is
45/// asked for. The loop is host-driven — the browser's timer pumps it — and
46/// the one thread there is is always the loop's, so a verb runs its body
47/// inline.
48
49namespace morph::exec {
50
51/// @brief Who turns an `IoLoop`.
52enum class IoLoopDriver : std::uint8_t {
53 /// The `IoLoop` starts one thread that runs the loop until it is destroyed.
55 /// No thread is started: the constructing thread turns the loop, and is the
56 /// loop's between turns as well as inside them. No other thread may turn
57 /// it; a debug build asserts when a task posted to it runs on another.
59};
60
61/// @brief Owns a `core::net::PlatformLoop` and, for `IoLoopDriver::OwnThread`
62/// natively, the one thread that runs it.
63///
64/// Cross-thread surface: `post()`, `runAndWait()`, `runningHere()`, `driver()`
65/// and `weak()`. Everything else about the loop — timers, sockets, flows — is
66/// touched only in tasks it runs, or, for a `Caller` loop, on its driving
67/// thread.
68///
69/// @par Lifetime
70/// Must outlive every component built on it: a component's destructor runs
71/// its close on the loop and waits for it, which a stopped loop would never
72/// run. Destroy the components first, then the `IoLoop`. A `Caller` loop and
73/// its components are destroyed on the driving thread, outside a turn.
74class IoLoop {
75 /// The loop, shared with the thread that runs it, so that an `IoLoop`
76 /// destroyed on its own thread can let that thread finish the turn it is in.
77 struct Impl {
78 ::core::net::PlatformLoop loop;
79#ifndef MORPH_IO_LOOP_HOST_DRIVEN
80 /// The thread that turns a `Caller` loop; no thread for `OwnThread`.
81 std::thread::id caller;
82#endif
83 };
84
85public:
86 /// @brief A non-owning handle that posts to the loop only while it exists.
87 ///
88 /// For a callback that another executor runs and that may outlive the
89 /// component that armed it — a `RemoteServer` reply arriving on a pool
90 /// thread after its transport has gone.
91 class Weak {
92 public:
93 /// @brief Posts @p task if the loop still exists.
94 /// @tparam F A copyable callable taking no arguments.
95 /// @param task What to run on the loop thread.
96 /// @return `false` if the loop is gone and @p task was dropped.
97 template <class F>
98 bool post(F&& task) const {
99 std::shared_ptr<Impl> const impl = _impl.lock();
100 if (!impl) {
101 return false;
102 }
103 impl->loop.post(guarded(*impl, std::forward<F>(task)));
104 return true;
105 }
106
107 private:
108 friend class IoLoop;
109 explicit Weak(std::weak_ptr<Impl> impl) : _impl{std::move(impl)} {}
110 std::weak_ptr<Impl> _impl;
111 };
112
113#ifndef MORPH_IO_LOOP_HOST_DRIVEN
114 /// @brief Creates the loop and, with `IoLoopDriver::OwnThread`, starts the
115 /// thread that runs it.
116 /// @param driver Who turns the loop. `Caller` starts no thread and makes the
117 /// constructing thread the loop's.
118 explicit IoLoop(IoLoopDriver driver = IoLoopDriver::OwnThread) : _impl{std::make_shared<Impl>()}, _driver{driver} {
120 _impl->caller = std::this_thread::get_id();
121 } else {
122 _thread = std::thread{[impl = _impl] {
123 // One name for every IoLoop thread: an application runs one, and
124 // its sockets, deadlines and probe all turn on it.
125 MORPH_THREAD_NAME("morph.io");
126 impl->loop.run();
127 }};
128 }
129 }
130
131 /// @brief Stops the loop and joins its thread; a `Caller` loop is
132 /// destroyed in place.
133 ///
134 /// Work still queued is dropped by the loop's own teardown, not run. On the
135 /// loop's own thread — a task that dropped the last owner — an `OwnThread`
136 /// loop cannot join: it asks the loop to stop and lets the thread end with
137 /// the turn it is in, the loop kept alive by the thread's own share of it.
138 /// A `Caller` loop has no thread to stop: destroyed outside a turn nothing
139 /// is running it, so its teardown is serialised with dispatch. Destroyed
140 /// inside one of its own turns, that turn would return into a destroyed
141 /// loop; an assertion refuses it.
143 if (_driver == IoLoopDriver::Caller) {
144 assert(!::morph::exec::runningOn(static_cast<::core::async::IExecutor const&>(_impl->loop)) &&
145 "a caller-driven IoLoop must be destroyed outside its own turns");
146 return;
147 }
148 _impl->loop.stop();
149 if (_thread.get_id() == std::this_thread::get_id()) {
150 _thread.detach();
151 } else if (_thread.joinable()) {
152 _thread.join();
153 }
154 }
155#else
156 /// @brief Creates the loop. Starts no thread whichever driver is asked for:
157 /// the browser's timer pumps it.
158 /// @param driver Recorded and reported by `driver()`; both drivers behave
159 /// alike here.
161 : _impl{std::make_shared<Impl>()}, _driver{driver} {}
162
163 /// @brief Destroys the loop, dropping whatever it still holds.
164 ~IoLoop() = default;
165#endif
166
167 IoLoop(const IoLoop&) = delete;
168 IoLoop& operator=(const IoLoop&) = delete;
169 IoLoop(IoLoop&&) = delete;
170 IoLoop& operator=(IoLoop&&) = delete;
171
172 /// @brief The loop itself, for a component arming timers or opening
173 /// sockets from inside one of its tasks, and for the driving thread
174 /// of a `Caller` loop to turn it.
175 /// @return The platform loop; valid for this object's lifetime.
176 [[nodiscard]] ::core::net::EventLoop& loop() noexcept { return _impl->loop; }
177
178 /// @brief Who turns this loop.
179 /// @return The driver passed at construction.
180 [[nodiscard]] IoLoopDriver driver() const noexcept { return _driver; }
181
182 /// @brief Whether the calling thread may touch the loop's state directly.
183 ///
184 /// Natively that is the loop's own `ExecutorScope`, stated for every turn,
185 /// and — for a `Caller` loop — also the driving thread between turns, where
186 /// nothing else can be running the loop. Under single-threaded WebAssembly
187 /// there is one thread, so the answer is always yes.
188 /// @return True inside one of the loop's tasks, or on a `Caller` loop's
189 /// driving thread.
190 [[nodiscard]] bool runningHere() const noexcept {
191#ifndef MORPH_IO_LOOP_HOST_DRIVEN
192 // An OwnThread loop's `caller` is the default id, which names no thread.
193 return ::morph::exec::runningOn(static_cast<::core::async::IExecutor const&>(_impl->loop)) ||
194 std::this_thread::get_id() == _impl->caller;
195#else
196 return true;
197#endif
198 }
199
200 /// @brief Queues @p task to run on the loop thread, in a later turn.
201 ///
202 /// A throw out of @p task is logged and swallowed: one escaping a turn
203 /// would end the loop's thread.
204 /// @tparam F A copyable callable taking no arguments.
205 /// @param task What to run.
206 template <class F>
207 void post(F&& task) {
208 _impl->loop.post(guarded(*_impl, std::forward<F>(task)));
209 }
210
211 /// @brief Runs @p task on the loop and returns once it has run.
212 ///
213 /// Inline where `runningHere()` holds, so a task of the loop — or a
214 /// `Caller` loop's driving thread — never waits on itself; inline on a
215 /// `Caller` loop's driving thread between turns, it runs inside the loop's
216 /// `ExecutorScope`, so `runningOn()` agrees with `runningHere()` there. For
217 /// teardown and for the few verbs that must answer with the loop's own
218 /// result.
219 ///
220 /// A throw out of @p task reaches the caller, inline or not. A task the
221 /// loop drops unrun — it was destroyed first — ends the wait rather than
222 /// hanging it, and the call returns without @p task having run.
223 /// @tparam F A copyable callable taking no arguments.
224 /// @param task What to run.
225 template <class F>
226 void runAndWait(F&& task) {
227 if (runningHere()) {
228 if (::morph::exec::runningOn(static_cast<::core::async::IExecutor const&>(_impl->loop))) {
229 std::forward<F>(task)();
230 } else {
231 ::core::async::ExecutorScope const scope{_impl->loop};
232 std::forward<F>(task)();
233 }
234 return;
235 }
236 // The posted task holds the only share of the promise, so a task the
237 // loop drops unrun breaks it, and that is what ends the wait.
238 auto promise = std::make_shared<std::promise<std::exception_ptr>>();
239 std::future<std::exception_ptr> finished = promise->get_future();
240 post([task = std::forward<F>(task), done = std::move(promise)]() mutable {
241 std::exception_ptr failure;
242 try {
243 task();
244 } catch (...) {
245 failure = std::current_exception();
246 }
247 done->set_value(std::move(failure));
248 });
249 std::exception_ptr failure;
250 try {
251 failure = finished.get();
252 } catch (const std::future_error&) { // NOLINT(bugprone-empty-catch)
253 // Broken: dropped unrun, so there is nothing left to wait for.
254 }
255 if (failure) {
256 std::rethrow_exception(failure);
257 }
258 }
259
260 /// @brief A handle that posts only while this loop exists.
261 /// @return The weak handle.
262 [[nodiscard]] Weak weak() const { return Weak{_impl}; }
263
264private:
265 /// Wraps @p task to log a throw rather than let it end the turn, and, in a
266 /// debug build, to refuse a `Caller` loop turned by a thread other than the
267 /// one that constructed it: two threads taking turns would race every
268 /// component's state, which `runningHere()` tells each of them it owns.
269 template <class F>
270 static auto guarded([[maybe_unused]] Impl const& impl, F&& task) {
271#ifndef MORPH_IO_LOOP_HOST_DRIVEN
272 return [caller = impl.caller, task = std::forward<F>(task)]() mutable noexcept {
273 assert((caller == std::thread::id{} || caller == std::this_thread::get_id()) &&
274 "a caller-driven IoLoop is turned only by the thread that constructed it");
275 static_cast<void>(caller);
276 runLogged(task);
277 };
278#else
279 return [task = std::forward<F>(task)]() mutable noexcept { runLogged(task); };
280#endif
281 }
282
283 template <class F>
284 static void runLogged(F& task) noexcept {
285 try {
286 task();
287 } catch (const std::exception& exc) {
288 ::morph::log::logError("[io-loop] task threw: {}", exc.what());
289 } catch (...) {
290 ::morph::log::logError("[io-loop] task threw an unknown exception");
291 }
292 }
293
294 std::shared_ptr<Impl> _impl;
295 IoLoopDriver _driver;
296#ifndef MORPH_IO_LOOP_HOST_DRIVEN
297 std::thread _thread;
298#endif
299};
300
301} // namespace morph::exec
A non-owning handle that posts to the loop only while it exists.
Definition io_loop.hpp:91
bool post(F &&task) const
Posts task if the loop still exists.
Definition io_loop.hpp:98
void runAndWait(F &&task)
Runs task on the loop and returns once it has run.
Definition io_loop.hpp:226
::core::net::EventLoop & loop() noexcept
The loop itself, for a component arming timers or opening sockets from inside one of its tasks,...
Definition io_loop.hpp:176
void post(F &&task)
Queues task to run on the loop thread, in a later turn.
Definition io_loop.hpp:207
IoLoop(IoLoopDriver driver=IoLoopDriver::OwnThread)
Creates the loop and, with IoLoopDriver::OwnThread, starts the thread that runs it.
Definition io_loop.hpp:118
IoLoopDriver driver() const noexcept
Who turns this loop.
Definition io_loop.hpp:180
bool runningHere() const noexcept
Whether the calling thread may touch the loop's state directly.
Definition io_loop.hpp:190
~IoLoop()
Stops the loop and joins its thread; a Caller loop is destroyed in place.
Definition io_loop.hpp:142
Weak weak() const
A handle that posts only while this loop exists.
Definition io_loop.hpp:262
IoLoopDriver
Who turns an IoLoop.
Definition io_loop.hpp:52
@ OwnThread
The IoLoop starts one thread that runs the loop until it is destroyed.
Definition io_loop.hpp:54
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_THREAD_NAME(name)
Names the calling thread in the capture.
Definition profiler.hpp:137