include/boost/corosio/native/detail/reactor/reactor_scheduler.hpp

98.4% Lines (379 / 385, 2 excl) 100.0% Functions (49 / 49, 2 excl)
reactor_scheduler.hpp
f(x) Functions (51)
Function Calls Lines Blocks
boost::corosio::detail::reactor_retirable::~reactor_retirable() :69 11937x 100.0% 100.0% boost::corosio::detail::reactor_find_context(boost::corosio::detail::reactor_scheduler const*) :111 894865x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::notify_reparked() const :228 467x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::inline_budget_initial() const :275 13311x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::scheduler_locking_disabled() const :281 1448x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::configure_threading(boost::corosio::detail::scheduler::threading_config) :286 4415x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::reactor_scheduler() :303 4427x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::task_op::operator()() :364 – – – boost::corosio::detail::reactor_scheduler::task_op::destroy() :365 – – – boost::corosio::detail::reactor_thread_context_guard::reactor_thread_context_guard(boost::corosio::detail::reactor_scheduler const*) :419 13311x 100.0% 100.0% boost::corosio::detail::reactor_thread_context_guard::~reactor_thread_context_guard() :433 13311x 100.0% 100.0% boost::corosio::detail::reactor_scheduler_context::reactor_scheduler_context(boost::corosio::detail::reactor_scheduler const*, boost::corosio::detail::reactor_scheduler_context*) :441 13311x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::configure_reactor(unsigned int, unsigned int, unsigned int, unsigned int) :453 54x 100.0% 87.0% boost::corosio::detail::reactor_scheduler::reset_inline_budget() const :478 93466x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::try_consume_inline_budget() const :506 383663x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post(std::__n4861::coroutine_handle<void>) const :522 3672x 100.0% 84.0% boost::corosio::detail::reactor_scheduler::post(std::__n4861::coroutine_handle<void>) const::post_handler::post_handler(std::__n4861::coroutine_handle<void>) :528 3672x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post(std::__n4861::coroutine_handle<void>) const::post_handler::~post_handler() :529 7344x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post(std::__n4861::coroutine_handle<void>) const::post_handler::operator()() :531 3656x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post(std::__n4861::coroutine_handle<void>) const::post_handler::destroy() :538 16x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post(boost::corosio::detail::scheduler_op*) const :565 101249x 100.0% 87.0% boost::corosio::detail::reactor_scheduler::post(boost::capy::continuation&) const :582 252532x 100.0% 87.0% boost::corosio::detail::reactor_scheduler::running_in_this_thread() const :599 24016x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::stop() :605 8687x 100.0% 82.0% boost::corosio::detail::reactor_scheduler::stopped() const :617 3531x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::restart() :623 6010x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::run() :629 6799x 100.0% 86.0% boost::corosio::detail::reactor_scheduler::run_one() :654 130x 100.0% 70.0% boost::corosio::detail::reactor_scheduler::wait_one(long) :668 6118x 100.0% 70.0% boost::corosio::detail::reactor_scheduler::poll() :682 1327x 100.0% 76.0% boost::corosio::detail::reactor_scheduler::poll_one() :707 58x 100.0% 70.0% boost::corosio::detail::reactor_scheduler::work_started() :721 67379x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::work_finished() :727 112666x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::compensating_work_started() const :734 34388x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post_counted(boost::corosio::detail::scheduler_op*) const :742 1979x 100.0% 85.0% boost::corosio::detail::reactor_scheduler::post_deferred_completions(boost::corosio::detail::ready_queue&) const :756 16143x 60.0% 59.0% boost::corosio::detail::reactor_scheduler::retire_descriptor(boost::corosio::detail::reactor_retirable&, boost::corosio::detail::object_ref) const :773 23013x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::settle_retired(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :793 299256x 100.0% 94.0% boost::corosio::detail::reactor_scheduler::shutdown_drain() :826 4415x 100.0% 83.0% boost::corosio::detail::reactor_scheduler::~reactor_scheduler() :843 4427x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::destroy_queued(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) :851 8842x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::signal_all(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :875 12042x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::maybe_unlock_and_signal_one(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :882 39246x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::unlock_and_signal_one(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :898 661585x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::clear_signal() const :909 812x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::wait_for_signal(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :915 802x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::wait_for_signal_for(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&, long) const :926 10x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::wake_one_thread_and_unlock(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :937 39246x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::work_cleanup::~work_cleanup() :955 399857x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::task_cleanup::~task_cleanup() :972 294844x 90.0% 91.0% boost::corosio::detail::reactor_scheduler::do_one(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&, long, boost::corosio::detail::reactor_scheduler_context&) :990 410329x 97.9% 84.0%
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2026 Steve Gerbino
3 //
4 // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 //
7 // Official repository: https://github.com/cppalliance/corosio
8 //
9
10 #ifndef BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SCHEDULER_HPP
11 #define BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SCHEDULER_HPP
12
13 #include <boost/corosio/detail/config.hpp>
14 #include <boost/corosio/detail/object_ref.hpp>
15 #include <boost/capy/ex/execution_context.hpp>
16 #include <boost/capy/ex/abandon.hpp>
17
18 #include <boost/corosio/detail/ready_queue.hpp>
19 #include <boost/corosio/detail/scheduler.hpp>
20 #include <boost/corosio/detail/scheduler_op.hpp>
21 #include <boost/corosio/detail/thread_local_ptr.hpp>
22 #include <boost/corosio/detail/timer_service.hpp>
23
24 #include <atomic>
25 #include <chrono>
26 #include <coroutine>
27 #include <cstddef>
28 #include <cstdint>
29 #include <limits>
30 #include <memory>
31 #include <stdexcept>
32
33 #include <boost/corosio/detail/conditionally_enabled_mutex.hpp>
34 #include <boost/corosio/detail/conditionally_enabled_event.hpp>
35
36 namespace boost::corosio::detail {
37
38 // Forward declarations
39 class reactor_scheduler;
40
41 /** A deregistered descriptor whose owner must outlive the reactor pass
42 that may still hold it.
43
44 An event the reactor has already read from the kernel can name a
45 descriptor that another thread closes before the reactor queues it.
46 The closing thread hands over a reference instead of dropping it, so
47 the owner is neither freed nor recycled until that pass ends.
48 */
49 struct reactor_retirable
50 {
51 // All four fields are guarded by the scheduler's mutex.
52
53 /// Next in the scheduler's retired list.
54 reactor_retirable* retired_next_ = nullptr;
55
56 /// Owner reference held until the pass ends.
57 object_ref retired_ref_;
58
59 /// Next in a settling thread's list of references to drop.
60 reactor_retirable* dropped_next_ = nullptr;
61
62 /// Owner reference waiting to be dropped outside the mutex.
63 object_ref dropped_ref_;
64
65 /// Move @p keep into a queued completion, if one is pending.
66 virtual void hand_off(object_ref& keep) noexcept = 0;
67
68 protected:
69 11937x ~reactor_retirable() = default;
70 };
71
72 /** Per-thread state for a reactor scheduler.
73
74 Each thread running a scheduler's event loop has one of these
75 on a thread-local stack. It holds a private work queue and
76 inline completion budget for speculative I/O fast paths.
77 */
78 struct BOOST_COROSIO_SYMBOL_VISIBLE reactor_scheduler_context
79 {
80 /// Scheduler this context belongs to.
81 reactor_scheduler const* key;
82
83 /// Next context frame on this thread's stack.
84 reactor_scheduler_context* next;
85
86 /// Private work queue for reduced contention.
87 ready_queue private_queue;
88
89 /// Unflushed work count for the private queue.
90 std::int64_t private_outstanding_work;
91
92 /// Remaining inline completions allowed this cycle.
93 int inline_budget;
94
95 /// Maximum inline budget (adaptive, 2-16).
96 int inline_budget_max;
97
98 /// True if no other thread absorbed queued work last cycle.
99 bool unassisted;
100
101 /// Construct a context frame linked to @a n.
102 reactor_scheduler_context(
103 reactor_scheduler const* k, reactor_scheduler_context* n);
104 };
105
106 /// Thread-local context stack for reactor schedulers.
107 inline thread_local_ptr<reactor_scheduler_context> reactor_context_stack;
108
109 /// Find the context frame for a scheduler on this thread.
110 inline reactor_scheduler_context*
111 894865x reactor_find_context(reactor_scheduler const* self) noexcept
112 {
113 895390x for (auto* c = reactor_context_stack.get(); c != nullptr; c = c->next)
114 {
115 832120x if (c->key == self)
116 831595x return c;
117 }
118 63270x return nullptr;
119 }
120
121 /** Non-template base for reactor-backed scheduler implementations.
122
123 Provides the complete threading model shared by epoll, kqueue,
124 and select schedulers: signal state machine, inline completion
125 budget, work counting, run/poll methods, and the do_one event
126 loop.
127
128 Derived classes provide platform-specific hooks by overriding:
129 - `run_task(lock, ctx)` to run the reactor poll
130 - `interrupt_reactor()` to wake a blocked reactor
131
132 De-templated from the original CRTP design to eliminate
133 duplicate instantiations when multiple backends are compiled
134 into the same binary. Virtual dispatch for run_task (called
135 once per reactor cycle, before a blocking syscall) has
136 negligible overhead.
137
138 @par Thread Safety
139 All public member functions are thread-safe.
140 */
141 class reactor_scheduler
142 : public scheduler
143 {
144 public:
145 using context_type = reactor_scheduler_context;
146 using mutex_type = conditionally_enabled_mutex;
147 using lock_type = mutex_type::scoped_lock;
148 using event_type = conditionally_enabled_event;
149
150 /// Post a coroutine for deferred execution.
151 void post(std::coroutine_handle<> h) const override;
152
153 /// Post a scheduler operation for deferred execution.
154 void post(scheduler_op* h) const override;
155
156 /** Release an owner reference once no reactor pass can hold `d`.
157
158 Called after a descriptor is deregistered. If a reactor pass is
159 running, `keep` is parked on `d` until that pass ends. Then, or
160 at once if no pass is running, it moves to `d`'s queued
161 invocation if `d` is queued, and is dropped otherwise.
162
163 @param d The deregistered descriptor.
164 @param keep A reference to the descriptor's owner.
165 */
166 void retire_descriptor(reactor_retirable& d, object_ref keep) const;
167
168 /// Post a continuation for deferred execution.
169 void post(capy::continuation&) const override;
170
171 /// Return true if called from a thread running this scheduler.
172 bool running_in_this_thread() const noexcept override;
173
174 /// Request the scheduler to stop dispatching handlers.
175 void stop() override;
176
177 /// Return true if the scheduler has been stopped.
178 bool stopped() const noexcept override;
179
180 /// Reset the stopped state so `run()` can resume.
181 void restart() override;
182
183 /// Run the event loop until no work remains.
184 std::size_t run() override;
185
186 /// Run until one handler completes or no work remains.
187 std::size_t run_one() override;
188
189 /// Run until one handler completes or @a usec elapses.
190 std::size_t wait_one(long usec) override;
191
192 /// Run ready handlers without blocking.
193 std::size_t poll() override;
194
195 /// Run at most one ready handler without blocking.
196 std::size_t poll_one() override;
197
198 /// Increment the outstanding work count.
199 void work_started() noexcept override;
200
201 /// Decrement the outstanding work count, stopping on zero.
202 void work_finished() noexcept override;
203
204 /** Reset the thread's inline completion budget.
205
206 Called at the start of each posted completion handler to
207 grant a fresh budget for speculative inline completions.
208 */
209 void reset_inline_budget() const noexcept;
210
211 /** Consume one unit of inline budget if available.
212
213 @return True if budget was available and consumed.
214 */
215 bool try_consume_inline_budget() const noexcept;
216
217 /** Offset a forthcoming work_finished from work_cleanup.
218
219 Called by descriptor_state when all I/O returned EAGAIN and
220 no handler will be executed. Must be called from a scheduler
221 thread.
222 */
223 /** Note that an op was parked outside the usual registration path.
224
225 A reactor that builds its poll set from parked ops must rebuild
226 it; one the kernel tracks needs nothing.
227 */
228 467x virtual void notify_reparked() const {}
229
230 void compensating_work_started() const noexcept;
231
232 /** Post an operation whose work is already counted.
233
234 The posting thread touches nothing of the scheduler after this
235 returns, unlike a @ref post followed by @ref work_finished: once
236 posted, the operation can complete and let its context be
237 destroyed.
238
239 @pre work_started() was called for @p op.
240
241 @param op The operation to post.
242 */
243 void post_counted(scheduler_op* op) const;
244
245 /** Post completed operations for deferred invocation.
246
247 If called from a thread running this scheduler, operations
248 go to the thread's private queue (fast path). Otherwise,
249 operations are added to the global queue under mutex and a
250 waiter is signaled.
251
252 @pre work_started() must have been called for each operation.
253
254 @param ops Queue of operations to post.
255 */
256 void post_deferred_completions(ready_queue& ops) const;
257
258 /** Apply runtime configuration to the scheduler.
259
260 Called by `io_context` after construction. Values that do
261 not apply to this backend are silently ignored.
262
263 @param max_events Event buffer size for epoll/kqueue.
264 @param budget_init Starting inline completion budget.
265 @param budget_max Hard ceiling on adaptive budget ramp-up.
266 @param unassisted Budget when single-threaded.
267 */
268 virtual void configure_reactor(
269 unsigned max_events,
270 unsigned budget_init,
271 unsigned budget_max,
272 unsigned unassisted);
273
274 /// Return the configured initial inline budget.
275 13311x unsigned inline_budget_initial() const noexcept
276 {
277 13311x return inline_budget_initial_;
278 }
279
280 /// Return true when scheduler locking is disabled (fully-lockless tier).
281 1448x bool scheduler_locking_disabled() const noexcept override
282 {
283 1448x return scheduler_locking_disabled_;
284 }
285
286 4415x void configure_threading(threading_config cfg) noexcept override
287 {
288 4415x scheduler_locking_disabled_ = !cfg.scheduler_locking;
289 // reactor_io_locking takes effect at descriptor registration (see the
290 // register_descriptor overrides), not here.
291 4415x reactor_io_locking_ = cfg.reactor_io_locking;
292 4415x one_thread_ = cfg.one_thread;
293 4415x mutex_.set_enabled(cfg.scheduler_locking);
294 4415x cond_.set_enabled(cfg.scheduler_locking);
295 4415x }
296
297 protected:
298 timer_service* timer_svc_ = nullptr;
299 bool scheduler_locking_disabled_ = false;
300 bool reactor_io_locking_ = true;
301 bool one_thread_ = false;
302
303 4427x reactor_scheduler() = default;
304
305 /// Destroy, destroying whatever was posted after the drain.
306 ~reactor_scheduler() override;
307
308 /** Drain completed_ops during shutdown.
309
310 Pops all operations from the global queue and destroys them,
311 skipping the task sentinel. Signals all waiting threads.
312 Derived classes call this from their shutdown() override
313 before performing platform-specific cleanup.
314 */
315 void shutdown_drain();
316
317 /// Destroy every queued operation without running it.
318 void destroy_queued(lock_type& lock) noexcept;
319
320 /// RAII guard that re-inserts the task sentinel after `run_task`.
321 struct task_cleanup
322 {
323 reactor_scheduler const* sched;
324 lock_type* lock;
325 context_type& ctx;
326 ~task_cleanup();
327 };
328
329 mutable mutex_type mutex_{true};
330 mutable event_type cond_{true};
331 mutable ready_queue completed_ops_;
332 mutable std::atomic<std::int64_t> outstanding_work_{0};
333 std::atomic<bool> stopped_{false};
334 mutable std::atomic<bool> task_running_{false};
335 mutable bool task_interrupted_ = false;
336
337 // Descriptors retired during the running reactor pass (guarded by
338 // mutex_).
339 mutable reactor_retirable* retired_ = nullptr;
340
341 /// Settle every descriptor retired before the current pass ended.
342 /// @pre `lock` holds `mutex_`; it is held again on return.
343 void settle_retired(lock_type& lock) const;
344
345 // Runtime-configurable reactor tuning parameters.
346 // Defaults match the library's built-in values.
347 unsigned max_events_per_poll_ = 128;
348 unsigned inline_budget_initial_ = 2;
349 unsigned inline_budget_max_ = 16;
350 unsigned unassisted_budget_ = 4;
351
352 /// Bit 0 of `state_`: set when the condvar should be signaled.
353 static constexpr std::size_t signaled_bit = 1;
354
355 /// Increment per waiting thread in `state_`.
356 static constexpr std::size_t waiter_increment = 2;
357 mutable std::size_t state_ = 0;
358
359 /// Sentinel op that triggers a reactor poll when dequeued.
360 struct task_op final : scheduler_op
361 {
362 // LCOV_EXCL_START: the sentinel is intercepted by pointer
363 // identity; its virtuals exist for vtable completeness.
364 − void operator()() override {}
365 − void destroy() override {}
366 // LCOV_EXCL_STOP
367 };
368 task_op task_op_;
369
370 /** Run the platform-specific reactor poll.
371
372 @par Postconditions
373 `lock` is owned on return, however the poll ended. An
374 implementation that unlocks around the blocking call owes the
375 caller a matching re-acquire on every path out, including the
376 errors it retries rather than reports.
377 */
378 virtual void
379 run_task(lock_type& lock, context_type& ctx, long timeout_us) = 0;
380
381 /// Wake a blocked reactor (e.g. write to eventfd or pipe).
382 virtual void interrupt_reactor() const = 0;
383
384 private:
385 struct work_cleanup
386 {
387 reactor_scheduler* sched;
388 lock_type* lock;
389 context_type& ctx;
390 ~work_cleanup();
391 };
392
393 std::size_t do_one(lock_type& lock, long timeout_us, context_type& ctx);
394
395 void signal_all(lock_type& lock) const;
396 bool maybe_unlock_and_signal_one(lock_type& lock) const;
397 bool unlock_and_signal_one(lock_type& lock) const;
398 void clear_signal() const;
399 void wait_for_signal(lock_type& lock) const;
400 void wait_for_signal_for(lock_type& lock, long timeout_us) const;
401 void wake_one_thread_and_unlock(lock_type& lock) const;
402 };
403
404 /** RAII guard that pushes/pops a scheduler context frame.
405
406 On construction, pushes a new context frame onto the
407 thread-local stack. On destruction, drains any remaining
408 private queue items to the global queue and pops the frame.
409 */
410 struct reactor_thread_context_guard
411 {
412 /// The context frame managed by this guard.
413 reactor_scheduler_context frame_;
414
415 /// Marks the scheduler innermost for the executor's fast check.
416 running_scheduler_guard running_;
417
418 /// Construct the guard, pushing a frame for @a sched.
419 13311x explicit reactor_thread_context_guard(
420 reactor_scheduler const* sched) noexcept
421 13311x : frame_(sched, reactor_context_stack.get())
422 13311x , running_(sched)
423 {
424 13311x reactor_context_stack.set(&frame_);
425 13311x }
426
427 /** Destroy the guard, popping the frame.
428
429 The private queue is empty here by invariant: work_cleanup and
430 task_cleanup splice it to the global queue after every handler
431 and every reactor pass.
432 */
433 13311x ~reactor_thread_context_guard() noexcept
434 {
435 13311x reactor_context_stack.set(frame_.next);
436 13311x }
437 };
438
439 // ---- Inline implementations ------------------------------------------------
440
441 13311x inline reactor_scheduler_context::reactor_scheduler_context(
442 13311x reactor_scheduler const* k, reactor_scheduler_context* n)
443 13311x : key(k)
444 13311x , next(n)
445 13311x , private_outstanding_work(0)
446 13311x , inline_budget(0)
447 13311x , inline_budget_max(static_cast<int>(k->inline_budget_initial()))
448 13311x , unassisted(false)
449 {
450 13311x }
451
452 inline void
453 54x reactor_scheduler::configure_reactor(
454 unsigned max_events,
455 unsigned budget_init,
456 unsigned budget_max,
457 unsigned unassisted)
458 {
459 106x if (max_events < 1 ||
460 52x max_events > static_cast<unsigned>(std::numeric_limits<int>::max()))
461 2x throw std::out_of_range("max_events_per_poll must be in [1, INT_MAX]");
462 52x if (budget_max > static_cast<unsigned>(std::numeric_limits<int>::max()))
463 2x throw std::out_of_range("inline_budget_max must be in [0, INT_MAX]");
464
465 // Clamp initial and unassisted to budget_max.
466 50x if (budget_init > budget_max)
467 20x budget_init = budget_max;
468 50x if (unassisted > budget_max)
469 20x unassisted = budget_max;
470
471 50x max_events_per_poll_ = max_events;
472 50x inline_budget_initial_ = budget_init;
473 50x inline_budget_max_ = budget_max;
474 50x unassisted_budget_ = unassisted;
475 50x }
476
477 inline void
478 93466x reactor_scheduler::reset_inline_budget() const noexcept
479 {
480 // When budget is disabled (max==0), all paths below would no-op
481 // (inline_budget stays 0). Skip the TLS lookup entirely.
482 93466x if (inline_budget_max_ == 0)
483 58x return;
484 93408x if (auto* ctx = reactor_find_context(this))
485 {
486 // Cap when no other thread absorbed queued work
487 93408x if (ctx->unassisted)
488 {
489 92565x ctx->inline_budget_max = static_cast<int>(unassisted_budget_);
490 92565x ctx->inline_budget = static_cast<int>(unassisted_budget_);
491 92565x return;
492 }
493 // Ramp up when previous cycle fully consumed budget.
494 // max(1, ...) ensures the doubling escapes zero.
495 843x if (ctx->inline_budget == 0)
496 256x ctx->inline_budget_max =
497 256x (std::min)((std::max)(1, ctx->inline_budget_max) * 2,
498 512x static_cast<int>(inline_budget_max_));
499 587x else if (ctx->inline_budget < ctx->inline_budget_max)
500 195x ctx->inline_budget_max = static_cast<int>(inline_budget_initial_);
501 843x ctx->inline_budget = ctx->inline_budget_max;
502 }
503 }
504
505 inline bool
506 383663x reactor_scheduler::try_consume_inline_budget() const noexcept
507 {
508 383663x if (inline_budget_max_ == 0)
509 44x return false;
510 383619x if (auto* ctx = reactor_find_context(this))
511 {
512 383613x if (ctx->inline_budget > 0)
513 {
514 306662x --ctx->inline_budget;
515 306662x return true;
516 }
517 }
518 76957x return false;
519 }
520
521 inline void
522 3672x reactor_scheduler::post(std::coroutine_handle<> h) const
523 {
524 struct post_handler final : scheduler_op
525 {
526 std::coroutine_handle<> h_;
527
528 3672x explicit post_handler(std::coroutine_handle<> h) : h_(h) {}
529 7344x ~post_handler() override = default;
530
531 3656x void operator()() override
532 {
533 3656x auto saved = h_;
534 3656x delete this;
535 3656x saved.resume();
536 3656x }
537
538 16x void destroy() override
539 {
540 // A bare handle carries no owner to unwind from: it is
541 // destroyed directly.
542 16x auto saved = h_;
543 16x delete this;
544 16x saved.destroy();
545 16x }
546 };
547
548 3672x auto ph = std::make_unique<post_handler>(h);
549
550 3672x if (auto* ctx = reactor_find_context(this))
551 {
552 8x ++ctx->private_outstanding_work;
553 8x ctx->private_queue.push(ph.release());
554 8x return;
555 }
556
557 3664x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
558
559 3664x lock_type lock(mutex_);
560 3664x completed_ops_.push(ph.release());
561 3664x wake_one_thread_and_unlock(lock);
562 3672x }
563
564 inline void
565 101249x reactor_scheduler::post(scheduler_op* h) const
566 {
567 101249x if (auto* ctx = reactor_find_context(this))
568 {
569 99399x ++ctx->private_outstanding_work;
570 99399x ctx->private_queue.push(h);
571 99399x return;
572 }
573
574 1850x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
575
576 1850x lock_type lock(mutex_);
577 1850x completed_ops_.push(h);
578 1850x wake_one_thread_and_unlock(lock);
579 1850x }
580
581 inline void
582 252532x reactor_scheduler::post(capy::continuation& c) const
583 {
584 252532x if (auto* ctx = reactor_find_context(this))
585 {
586 220172x ++ctx->private_outstanding_work;
587 220172x ctx->private_queue.push(c);
588 220172x return;
589 }
590
591 32360x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
592
593 32360x lock_type lock(mutex_);
594 32360x completed_ops_.push(c);
595 32360x wake_one_thread_and_unlock(lock);
596 32360x }
597
598 inline bool
599 24016x reactor_scheduler::running_in_this_thread() const noexcept
600 {
601 24016x return reactor_find_context(this) != nullptr;
602 }
603
604 inline void
605 8687x reactor_scheduler::stop()
606 {
607 8687x lock_type lock(mutex_);
608 8687x if (!stopped_.load(std::memory_order_acquire))
609 {
610 7627x stopped_.store(true, std::memory_order_release);
611 7627x signal_all(lock);
612 7627x interrupt_reactor();
613 }
614 8687x }
615
616 inline bool
617 3531x reactor_scheduler::stopped() const noexcept
618 {
619 3531x return stopped_.load(std::memory_order_acquire);
620 }
621
622 inline void
623 6010x reactor_scheduler::restart()
624 {
625 6010x stopped_.store(false, std::memory_order_release);
626 6010x }
627
628 inline std::size_t
629 6799x reactor_scheduler::run()
630 {
631 13598x if (outstanding_work_.load(std::memory_order_acquire) == 0)
632 {
633 266x stop();
634 266x return 0;
635 }
636
637 6533x reactor_thread_context_guard ctx(this);
638 6533x lock_type lock(mutex_);
639
640 6533x std::size_t n = 0;
641 for (;;)
642 {
643 395574x if (!do_one(lock, -1, ctx.frame_))
644 6530x break;
645 389041x if (n != (std::numeric_limits<std::size_t>::max)())
646 389041x ++n;
647 389041x if (!lock.owns_lock())
648 88303x lock.lock();
649 }
650 6530x return n;
651 6536x }
652
653 inline std::size_t
654 130x reactor_scheduler::run_one()
655 {
656 260x if (outstanding_work_.load(std::memory_order_acquire) == 0)
657 {
658 3x stop();
659 3x return 0;
660 }
661
662 127x reactor_thread_context_guard ctx(this);
663 127x lock_type lock(mutex_);
664 127x return do_one(lock, -1, ctx.frame_);
665 127x }
666
667 inline std::size_t
668 6118x reactor_scheduler::wait_one(long usec)
669 {
670 12236x if (outstanding_work_.load(std::memory_order_acquire) == 0)
671 {
672 818x stop();
673 818x return 0;
674 }
675
676 5300x reactor_thread_context_guard ctx(this);
677 5300x lock_type lock(mutex_);
678 5300x return do_one(lock, usec, ctx.frame_);
679 5300x }
680
681 inline std::size_t
682 1327x reactor_scheduler::poll()
683 {
684 2654x if (outstanding_work_.load(std::memory_order_acquire) == 0)
685 {
686 29x stop();
687 29x return 0;
688 }
689
690 1298x reactor_thread_context_guard ctx(this);
691 1298x lock_type lock(mutex_);
692
693 1298x std::size_t n = 0;
694 for (;;)
695 {
696 9275x if (!do_one(lock, 0, ctx.frame_))
697 1298x break;
698 7977x if (n != (std::numeric_limits<std::size_t>::max)())
699 7977x ++n;
700 7977x if (!lock.owns_lock())
701 7975x lock.lock();
702 }
703 1298x return n;
704 1298x }
705
706 inline std::size_t
707 58x reactor_scheduler::poll_one()
708 {
709 116x if (outstanding_work_.load(std::memory_order_acquire) == 0)
710 {
711 5x stop();
712 5x return 0;
713 }
714
715 53x reactor_thread_context_guard ctx(this);
716 53x lock_type lock(mutex_);
717 53x return do_one(lock, 0, ctx.frame_);
718 53x }
719
720 inline void
721 67379x reactor_scheduler::work_started() noexcept
722 {
723 67379x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
724 67379x }
725
726 inline void
727 112666x reactor_scheduler::work_finished() noexcept
728 {
729 225332x if (outstanding_work_.fetch_sub(1, std::memory_order_acq_rel) == 1)
730 7501x stop();
731 112666x }
732
733 inline void
734 34388x reactor_scheduler::compensating_work_started() const noexcept
735 {
736 34388x auto* ctx = reactor_find_context(this);
737 34388x if (ctx)
738 34386x ++ctx->private_outstanding_work;
739 34388x }
740
741 inline void
742 1979x reactor_scheduler::post_counted(scheduler_op* op) const
743 {
744 1979x if (auto* ctx = reactor_find_context(this))
745 {
746 607x ctx->private_queue.push(op);
747 607x return;
748 }
749
750 1372x lock_type lock(mutex_);
751 1372x completed_ops_.push(op);
752 1372x wake_one_thread_and_unlock(lock);
753 1372x }
754
755 inline void
756 16143x reactor_scheduler::post_deferred_completions(ready_queue& ops) const
757 {
758 16143x if (ops.empty())
759 16143x return;
760
761 2x if (auto* ctx = reactor_find_context(this))
762 {
763 2x ctx->private_queue.splice(ops);
764 2x return;
765 }
766
767 ✗ lock_type lock(mutex_);
768 ✗ completed_ops_.splice(ops);
769 ✗ wake_one_thread_and_unlock(lock);
770 ✗ }
771
772 inline void
773 23013x reactor_scheduler::retire_descriptor(
774 reactor_retirable& d, object_ref keep) const
775 {
776 23013x lock_type lock(mutex_);
777 23013x if (!task_running_.load(std::memory_order_relaxed))
778 {
779 // No pass can hold an event, but an earlier one may have
780 // queued d.
781 17560x d.hand_off(keep);
782 17560x return;
783 }
784 // Already parked for this pass: the held reference suffices.
785 5453x if (d.retired_ref_)
786 2x return;
787 5451x d.retired_ref_ = std::move(keep);
788 5451x d.retired_next_ = retired_;
789 5451x retired_ = &d;
790 23013x }
791
792 inline void
793 299256x reactor_scheduler::settle_retired(lock_type& lock) const
794 {
795 // Handed off under the lock, since a later pass may retire the same
796 // descriptor again as soon as it is released. A reference that is
797 // not handed off may be the owner's last, and its release recycles
798 // through the pool mutex, so it is dropped with the lock released.
799 299256x reactor_retirable* dropped = nullptr;
800 304707x while (auto* d = retired_)
801 {
802 5451x retired_ = d->retired_next_;
803 5451x d->retired_next_ = nullptr;
804 5451x auto keep = std::move(d->retired_ref_);
805 5451x d->hand_off(keep);
806 // A drop already pending keeps the owner alive, so this one
807 // cannot be the last and is released here.
808 5451x if (!keep || d->dropped_ref_)
809 504x continue;
810 4947x d->dropped_ref_ = std::move(keep);
811 4947x d->dropped_next_ = dropped;
812 4947x dropped = d;
813 10902x }
814 304203x while (auto* d = dropped)
815 {
816 4947x dropped = d->dropped_next_;
817 4947x d->dropped_next_ = nullptr;
818 4947x auto keep = std::move(d->dropped_ref_);
819 4947x lock.unlock();
820 4947x keep.reset();
821 4947x lock.lock();
822 4947x }
823 299256x }
824
825 inline void
826 4415x reactor_scheduler::shutdown_drain()
827 {
828 // This scheduler's constructor registered the timer service, so
829 // the context shuts it down later; its waiters' frames must be
830 // destroyed before the drain, which then destroys whatever their
831 // destructors post. The context's own call later finds no waiter.
832 4415x if (timer_svc_)
833 4415x timer_svc_->shutdown();
834
835 4415x wait_cancels_posting();
836
837 4415x lock_type lock(mutex_);
838 4415x settle_retired(lock);
839 4415x destroy_queued(lock);
840 4415x signal_all(lock);
841 4415x }
842
843 4427x inline reactor_scheduler::~reactor_scheduler()
844 {
845 // Service destructors run after the drain and may still post.
846 4427x lock_type lock(mutex_);
847 4427x destroy_queued(lock);
848 4427x }
849
850 inline void
851 8842x reactor_scheduler::destroy_queued(lock_type& lock) noexcept
852 {
853 22603x while (auto e = completed_ops_.pop())
854 {
855 13761x if (ready_is_continuation(e))
856 {
857 7808x lock.unlock();
858 7808x if (auto* c = ready_as_cont(e); c->h)
859 7808x capy::abandon(*c);
860 7808x lock.lock();
861 }
862 else
863 {
864 5953x auto* op = ready_as_op(e);
865 5953x if (op == &task_op_)
866 4412x continue;
867 1541x lock.unlock();
868 1541x op->destroy();
869 1541x lock.lock();
870 }
871 13761x }
872 8842x }
873
874 inline void
875 12042x reactor_scheduler::signal_all(lock_type&) const
876 {
877 12042x state_ |= signaled_bit;
878 12042x cond_.notify_all();
879 12042x }
880
881 inline bool
882 39246x reactor_scheduler::maybe_unlock_and_signal_one(lock_type& lock) const
883 {
884 39246x state_ |= signaled_bit;
885 39246x if (state_ > signaled_bit)
886 {
887 // Signalled before unlocking: a post from another context's
888 // thread may be the last touch of this scheduler before the
889 // posted work runs, finishes and lets it be destroyed.
890 164x cond_.notify_one();
891 164x lock.unlock();
892 164x return true;
893 }
894 39082x return false;
895 }
896
897 inline bool
898 661585x reactor_scheduler::unlock_and_signal_one(lock_type& lock) const
899 {
900 661585x state_ |= signaled_bit;
901 661585x bool have_waiters = state_ > signaled_bit;
902 661585x lock.unlock();
903 661585x if (have_waiters)
904 2856x cond_.notify_one();
905 661585x return have_waiters;
906 }
907
908 inline void
909 812x reactor_scheduler::clear_signal() const
910 {
911 812x state_ &= ~signaled_bit;
912 812x }
913
914 inline void
915 802x reactor_scheduler::wait_for_signal(lock_type& lock) const
916 {
917 1670x while ((state_ & signaled_bit) == 0)
918 {
919 868x state_ += waiter_increment;
920 868x cond_.wait(lock);
921 868x state_ -= waiter_increment;
922 }
923 802x }
924
925 inline void
926 10x reactor_scheduler::wait_for_signal_for(lock_type& lock, long timeout_us) const
927 {
928 10x if ((state_ & signaled_bit) == 0)
929 {
930 10x state_ += waiter_increment;
931 10x cond_.wait_for(lock, std::chrono::microseconds(timeout_us));
932 10x state_ -= waiter_increment;
933 }
934 10x }
935
936 inline void
937 39246x reactor_scheduler::wake_one_thread_and_unlock(lock_type& lock) const
938 {
939 39246x if (maybe_unlock_and_signal_one(lock))
940 164x return;
941
942 39082x if (task_running_.load(std::memory_order_relaxed) && !task_interrupted_)
943 {
944 1720x task_interrupted_ = true;
945 // Under the lock, for the same reason as the signal above.
946 1720x interrupt_reactor();
947 1720x lock.unlock();
948 }
949 else
950 {
951 37362x lock.unlock();
952 }
953 }
954
955 399857x inline reactor_scheduler::work_cleanup::~work_cleanup()
956 {
957 399857x std::int64_t produced = ctx.private_outstanding_work;
958 399857x if (produced > 1)
959 972x sched->outstanding_work_.fetch_add(
960 produced - 1, std::memory_order_relaxed);
961 398885x else if (produced < 1)
962 65639x sched->work_finished();
963 399857x ctx.private_outstanding_work = 0;
964
965 399857x if (!ctx.private_queue.empty())
966 {
967 301147x lock->lock();
968 301147x sched->completed_ops_.splice(ctx.private_queue);
969 }
970 399857x }
971
972 294844x inline reactor_scheduler::task_cleanup::~task_cleanup()
973 {
974 294844x if (ctx.private_outstanding_work > 0)
975 {
976 10189x sched->outstanding_work_.fetch_add(
977 10189x ctx.private_outstanding_work, std::memory_order_relaxed);
978 10189x ctx.private_outstanding_work = 0;
979 }
980
981 294844x if (!ctx.private_queue.empty())
982 {
983 10189x if (!lock->owns_lock())
984 ✗ lock->lock();
985 10189x sched->completed_ops_.splice(ctx.private_queue);
986 }
987 294844x }
988
989 inline std::size_t
990 410329x reactor_scheduler::do_one(lock_type& lock, long timeout_us, context_type& ctx)
991 {
992 for (;;)
993 {
994 703343x if (stopped_.load(std::memory_order_acquire))
995 6705x return 0;
996
997 696638x std::uintptr_t e = completed_ops_.pop();
998 696638x scheduler_op* op = ready_is_continuation(e) ? nullptr : ready_as_op(e);
999
1000 // Handle reactor sentinel — time to poll for I/O
1001 696638x if (op == &task_op_)
1002 {
1003 295969x bool more_handlers = !completed_ops_.empty();
1004
1005 328453x if (!more_handlers &&
1006 64968x (outstanding_work_.load(std::memory_order_acquire) == 0 ||
1007 timeout_us == 0))
1008 {
1009 1125x completed_ops_.push(&task_op_);
1010 1125x return 0;
1011 }
1012
1013 294844x long task_timeout_us = more_handlers ? 0 : timeout_us;
1014 294844x task_interrupted_ = task_timeout_us == 0;
1015 294844x task_running_.store(true, std::memory_order_release);
1016
1017 // Wake a peer to take the pending handlers while this thread
1018 // polls the reactor; skipped when one_thread_ (no peer exists).
1019 294844x if (more_handlers && !one_thread_)
1020 263474x unlock_and_signal_one(lock);
1021
1022 try
1023 {
1024 294844x run_task(lock, ctx, task_timeout_us);
1025 }
1026 3x catch (...)
1027 {
1028 3x task_running_.store(false, std::memory_order_relaxed);
1029 3x throw;
1030 3x }
1031
1032 294841x task_running_.store(false, std::memory_order_relaxed);
1033 294841x completed_ops_.push(&task_op_);
1034 294841x settle_retired(lock);
1035 294841x if (timeout_us > 0)
1036 2639x return 0;
1037 292202x continue;
1038 292202x }
1039
1040 // Handle ready entry (op or continuation)
1041 400669x if (e != 0)
1042 {
1043 399857x bool more = !completed_ops_.empty();
1044
1045 399857x if (more && !one_thread_)
1046 {
1047 // Wake a peer for the remaining work; unassisted if none
1048 // was parked to take it.
1049 398111x ctx.unassisted = !unlock_and_signal_one(lock);
1050 }
1051 else
1052 {
1053 // No peer to wake (one_thread_, or nothing more queued).
1054 1746x ctx.unassisted = more;
1055 1746x lock.unlock();
1056 }
1057
1058 399857x [[maybe_unused]] work_cleanup on_exit{this, &lock, ctx};
1059
1060 399857x if (ready_is_continuation(e))
1061 244724x ready_as_cont(e)->h.resume();
1062 else
1063 155133x (*op)();
1064 399857x return 1;
1065 399857x }
1066
1067 1624x if (outstanding_work_.load(std::memory_order_acquire) == 0 ||
1068 timeout_us == 0)
1069 ✗ return 0;
1070
1071 812x clear_signal();
1072 812x if (timeout_us < 0)
1073 802x wait_for_signal(lock);
1074 else
1075 10x wait_for_signal_for(lock, timeout_us);
1076 293014x }
1077 }
1078
1079 } // namespace boost::corosio::detail
1080
1081 #endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SCHEDULER_HPP
1082