morph
0.1.0
Asynchronous bridge between GUI thread and business models
Toggle main menu visibility
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
49
namespace
morph::exec {
50
51
/// @brief Who turns an `IoLoop`.
52
enum class
IoLoopDriver
: std::uint8_t {
53
/// The `IoLoop` starts one thread that runs the loop until it is destroyed.
54
OwnThread
,
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.
58
Caller
,
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.
74
class
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
85
public
:
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
} {
119
if
(
driver
==
IoLoopDriver::Caller
) {
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.
142
~IoLoop
() {
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.
160
explicit
IoLoop
(
IoLoopDriver
driver
=
IoLoopDriver::OwnThread
)
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
264
private
:
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
morph::exec::IoLoop::Weak
A non-owning handle that posts to the loop only while it exists.
Definition
io_loop.hpp:91
morph::exec::IoLoop::Weak::post
bool post(F &&task) const
Posts task if the loop still exists.
Definition
io_loop.hpp:98
morph::exec::IoLoop::runAndWait
void runAndWait(F &&task)
Runs task on the loop and returns once it has run.
Definition
io_loop.hpp:226
morph::exec::IoLoop::loop
::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
morph::exec::IoLoop::post
void post(F &&task)
Queues task to run on the loop thread, in a later turn.
Definition
io_loop.hpp:207
morph::exec::IoLoop::IoLoop
IoLoop(IoLoopDriver driver=IoLoopDriver::OwnThread)
Creates the loop and, with IoLoopDriver::OwnThread, starts the thread that runs it.
Definition
io_loop.hpp:118
morph::exec::IoLoop::driver
IoLoopDriver driver() const noexcept
Who turns this loop.
Definition
io_loop.hpp:180
morph::exec::IoLoop::runningHere
bool runningHere() const noexcept
Whether the calling thread may touch the loop's state directly.
Definition
io_loop.hpp:190
morph::exec::IoLoop::~IoLoop
~IoLoop()
Stops the loop and joins its thread; a Caller loop is destroyed in place.
Definition
io_loop.hpp:142
morph::exec::IoLoop::weak
Weak weak() const
A handle that posts only while this loop exists.
Definition
io_loop.hpp:262
morph::exec::IoLoopDriver
IoLoopDriver
Who turns an IoLoop.
Definition
io_loop.hpp:52
morph::exec::IoLoopDriver::Caller
@ Caller
Definition
io_loop.hpp:58
morph::exec::IoLoopDriver::OwnThread
@ OwnThread
The IoLoop starts one thread that runs the loop until it is destroyed.
Definition
io_loop.hpp:54
profiler.hpp
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
MORPH_THREAD_NAME
#define MORPH_THREAD_NAME(name)
Names the calling thread in the capture.
Definition
profiler.hpp:137
morph
core
io_loop.hpp
Generated by
1.17.0