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 11999x 100.0% 100.0% boost::corosio::detail::reactor_find_context(boost::corosio::detail::reactor_scheduler const*) :111 1025181x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::notify_reparked() const :228 458x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::inline_budget_initial() const :275 13155x 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 4413x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::reactor_scheduler() :303 4425x 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 13155x 100.0% 100.0% boost::corosio::detail::reactor_thread_context_guard::~reactor_thread_context_guard() :433 13155x 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 13155x 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 111330x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::try_consume_inline_budget() const :506 472848x 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 119742x 100.0% 87.0% boost::corosio::detail::reactor_scheduler::post(boost::capy::continuation&) const :582 252861x 100.0% 87.0% boost::corosio::detail::reactor_scheduler::running_in_this_thread() const :599 23757x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::stop() :605 8364x 100.0% 82.0% boost::corosio::detail::reactor_scheduler::stopped() const :617 3552x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::restart() :623 5766x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::run() :629 6542x 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 6158x 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 67648x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::work_finished() :727 113181x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::compensating_work_started() const :734 39099x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::post_counted(boost::corosio::detail::scheduler_op*) const :742 1972x 100.0% 85.0% boost::corosio::detail::reactor_scheduler::post_deferred_completions(boost::corosio::detail::ready_queue&) const :756 16057x 60.0% 59.0% boost::corosio::detail::reactor_scheduler::retire_descriptor(boost::corosio::detail::reactor_retirable&, boost::corosio::detail::object_ref) const :773 22999x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::settle_retired(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :793 369989x 100.0% 94.0% boost::corosio::detail::reactor_scheduler::shutdown_drain() :826 4413x 100.0% 83.0% boost::corosio::detail::reactor_scheduler::~reactor_scheduler() :843 4425x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::destroy_queued(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) :851 8838x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::signal_all(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :875 11781x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::maybe_unlock_and_signal_one(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :882 39298x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::unlock_and_signal_one(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :898 686818x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::clear_signal() const :909 841x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::wait_for_signal(boost::corosio::detail::conditionally_enabled_mutex::scoped_lock&) const :915 831x 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 39298x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::work_cleanup::~work_cleanup() :955 423346x 100.0% 100.0% boost::corosio::detail::reactor_scheduler::task_cleanup::~task_cleanup() :972 365579x 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 433643x 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 11999x ~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 1025181x reactor_find_context(reactor_scheduler const* self) noexcept
112 {
113 1025754x for (auto* c = reactor_context_stack.get(); c != nullptr; c = c->next)
114 {
115 962679x if (c->key == self)
116 962106x return c;
117 }
118 63075x 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 458x 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 13155x unsigned inline_budget_initial() const noexcept
276 {
277 13155x 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 4413x void configure_threading(threading_config cfg) noexcept override
287 {
288 4413x scheduler_locking_disabled_ = !cfg.scheduler_locking;
289 // reactor_io_locking takes effect at descriptor registration (see the
290 // register_descriptor overrides), not here.
291 4413x reactor_io_locking_ = cfg.reactor_io_locking;
292 4413x one_thread_ = cfg.one_thread;
293 4413x mutex_.set_enabled(cfg.scheduler_locking);
294 4413x cond_.set_enabled(cfg.scheduler_locking);
295 4413x }
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 4425x 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 13155x explicit reactor_thread_context_guard(
420 reactor_scheduler const* sched) noexcept
421 13155x : frame_(sched, reactor_context_stack.get())
422 13155x , running_(sched)
423 {
424 13155x reactor_context_stack.set(&frame_);
425 13155x }
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 13155x ~reactor_thread_context_guard() noexcept
434 {
435 13155x reactor_context_stack.set(frame_.next);
436 13155x }
437 };
438
439 // ---- Inline implementations ------------------------------------------------
440
441 13155x inline reactor_scheduler_context::reactor_scheduler_context(
442 13155x reactor_scheduler const* k, reactor_scheduler_context* n)
443 13155x : key(k)
444 13155x , next(n)
445 13155x , private_outstanding_work(0)
446 13155x , inline_budget(0)
447 13155x , inline_budget_max(static_cast<int>(k->inline_budget_initial()))
448 13155x , unassisted(false)
449 {
450 13155x }
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 111330x 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 111330x if (inline_budget_max_ == 0)
483 58x return;
484 111272x if (auto* ctx = reactor_find_context(this))
485 {
486 // Cap when no other thread absorbed queued work
487 111272x if (ctx->unassisted)
488 {
489 110162x ctx->inline_budget_max = static_cast<int>(unassisted_budget_);
490 110162x ctx->inline_budget = static_cast<int>(unassisted_budget_);
491 110162x return;
492 }
493 // Ramp up when previous cycle fully consumed budget.
494 // max(1, ...) ensures the doubling escapes zero.
495 1110x if (ctx->inline_budget == 0)
496 330x ctx->inline_budget_max =
497 330x (std::min)((std::max)(1, ctx->inline_budget_max) * 2,
498 660x static_cast<int>(inline_budget_max_));
499 780x else if (ctx->inline_budget < ctx->inline_budget_max)
500 245x ctx->inline_budget_max = static_cast<int>(inline_budget_initial_);
501 1110x ctx->inline_budget = ctx->inline_budget_max;
502 }
503 }
504
505 inline bool
506 472848x reactor_scheduler::try_consume_inline_budget() const noexcept
507 {
508 472848x if (inline_budget_max_ == 0)
509 44x return false;
510 472804x if (auto* ctx = reactor_find_context(this))
511 {
512 472786x if (ctx->inline_budget > 0)
513 {
514 377932x --ctx->inline_budget;
515 377932x return true;
516 }
517 }
518 94872x 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 119742x reactor_scheduler::post(scheduler_op* h) const
566 {
567 119742x if (auto* ctx = reactor_find_context(this))
568 {
569 117685x ++ctx->private_outstanding_work;
570 117685x ctx->private_queue.push(h);
571 117685x return;
572 }
573
574 2057x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
575
576 2057x lock_type lock(mutex_);
577 2057x completed_ops_.push(h);
578 2057x wake_one_thread_and_unlock(lock);
579 2057x }
580
581 inline void
582 252861x reactor_scheduler::post(capy::continuation& c) const
583 {
584 252861x if (auto* ctx = reactor_find_context(this))
585 {
586 220651x ++ctx->private_outstanding_work;
587 220651x ctx->private_queue.push(c);
588 220651x return;
589 }
590
591 32210x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
592
593 32210x lock_type lock(mutex_);
594 32210x completed_ops_.push(c);
595 32210x wake_one_thread_and_unlock(lock);
596 32210x }
597
598 inline bool
599 23757x reactor_scheduler::running_in_this_thread() const noexcept
600 {
601 23757x return reactor_find_context(this) != nullptr;
602 }
603
604 inline void
605 8364x reactor_scheduler::stop()
606 {
607 8364x lock_type lock(mutex_);
608 8364x if (!stopped_.load(std::memory_order_acquire))
609 {
610 7368x stopped_.store(true, std::memory_order_release);
611 7368x signal_all(lock);
612 7368x interrupt_reactor();
613 }
614 8364x }
615
616 inline bool
617 3552x reactor_scheduler::stopped() const noexcept
618 {
619 3552x return stopped_.load(std::memory_order_acquire);
620 }
621
622 inline void
623 5766x reactor_scheduler::restart()
624 {
625 5766x stopped_.store(false, std::memory_order_release);
626 5766x }
627
628 inline std::size_t
629 6542x reactor_scheduler::run()
630 {
631 13084x if (outstanding_work_.load(std::memory_order_acquire) == 0)
632 {
633 238x stop();
634 238x return 0;
635 }
636
637 6304x reactor_thread_context_guard ctx(this);
638 6304x lock_type lock(mutex_);
639
640 6304x std::size_t n = 0;
641 for (;;)
642 {
643 418815x if (!do_one(lock, -1, ctx.frame_))
644 6301x break;
645 412511x if (n != (std::numeric_limits<std::size_t>::max)())
646 412511x ++n;
647 412511x if (!lock.owns_lock())
648 93067x lock.lock();
649 }
650 6301x return n;
651 6307x }
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 6158x reactor_scheduler::wait_one(long usec)
669 {
670 12316x if (outstanding_work_.load(std::memory_order_acquire) == 0)
671 {
672 785x stop();
673 785x return 0;
674 }
675
676 5373x reactor_thread_context_guard ctx(this);
677 5373x lock_type lock(mutex_);
678 5373x return do_one(lock, usec, ctx.frame_);
679 5373x }
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 67648x reactor_scheduler::work_started() noexcept
722 {
723 67648x outstanding_work_.fetch_add(1, std::memory_order_relaxed);
724 67648x }
725
726 inline void
727 113181x reactor_scheduler::work_finished() noexcept
728 {
729 226362x if (outstanding_work_.fetch_sub(1, std::memory_order_acq_rel) == 1)
730 7239x stop();
731 113181x }
732
733 inline void
734 39099x reactor_scheduler::compensating_work_started() const noexcept
735 {
736 39099x auto* ctx = reactor_find_context(this);
737 39099x if (ctx)
738 39097x ++ctx->private_outstanding_work;
739 39099x }
740
741 inline void
742 1972x reactor_scheduler::post_counted(scheduler_op* op) const
743 {
744 1972x if (auto* ctx = reactor_find_context(this))
745 {
746 605x ctx->private_queue.push(op);
747 605x return;
748 }
749
750 1367x lock_type lock(mutex_);
751 1367x completed_ops_.push(op);
752 1367x wake_one_thread_and_unlock(lock);
753 1367x }
754
755 inline void
756 16057x reactor_scheduler::post_deferred_completions(ready_queue& ops) const
757 {
758 16057x if (ops.empty())
759 16057x 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 22999x reactor_scheduler::retire_descriptor(
774 reactor_retirable& d, object_ref keep) const
775 {
776 22999x lock_type lock(mutex_);
777 22999x 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 17625x d.hand_off(keep);
782 17625x return;
783 }
784 // Already parked for this pass: the held reference suffices.
785 5374x if (d.retired_ref_)
786 7x return;
787 5367x d.retired_ref_ = std::move(keep);
788 5367x d.retired_next_ = retired_;
789 5367x retired_ = &d;
790 22999x }
791
792 inline void
793 369989x 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 369989x reactor_retirable* dropped = nullptr;
800 375356x while (auto* d = retired_)
801 {
802 5367x retired_ = d->retired_next_;
803 5367x d->retired_next_ = nullptr;
804 5367x auto keep = std::move(d->retired_ref_);
805 5367x 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 5367x if (!keep || d->dropped_ref_)
809 511x continue;
810 4856x d->dropped_ref_ = std::move(keep);
811 4856x d->dropped_next_ = dropped;
812 4856x dropped = d;
813 10734x }
814 374845x while (auto* d = dropped)
815 {
816 4856x dropped = d->dropped_next_;
817 4856x d->dropped_next_ = nullptr;
818 4856x auto keep = std::move(d->dropped_ref_);
819 4856x lock.unlock();
820 4856x keep.reset();
821 4856x lock.lock();
822 4856x }
823 369989x }
824
825 inline void
826 4413x 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 4413x if (timer_svc_)
833 4413x timer_svc_->shutdown();
834
835 4413x wait_cancels_posting();
836
837 4413x lock_type lock(mutex_);
838 4413x settle_retired(lock);
839 4413x destroy_queued(lock);
840 4413x signal_all(lock);
841 4413x }
842
843 4425x inline reactor_scheduler::~reactor_scheduler()
844 {
845 // Service destructors run after the drain and may still post.
846 4425x lock_type lock(mutex_);
847 4425x destroy_queued(lock);
848 4425x }
849
850 inline void
851 8838x reactor_scheduler::destroy_queued(lock_type& lock) noexcept
852 {
853 22593x while (auto e = completed_ops_.pop())
854 {
855 13755x 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 5947x auto* op = ready_as_op(e);
865 5947x if (op == &task_op_)
866 4410x continue;
867 1537x lock.unlock();
868 1537x op->destroy();
869 1537x lock.lock();
870 }
871 13755x }
872 8838x }
873
874 inline void
875 11781x reactor_scheduler::signal_all(lock_type&) const
876 {
877 11781x state_ |= signaled_bit;
878 11781x cond_.notify_all();
879 11781x }
880
881 inline bool
882 39298x reactor_scheduler::maybe_unlock_and_signal_one(lock_type& lock) const
883 {
884 39298x state_ |= signaled_bit;
885 39298x 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 215x cond_.notify_one();
891 215x lock.unlock();
892 215x return true;
893 }
894 39083x return false;
895 }
896
897 inline bool
898 686818x reactor_scheduler::unlock_and_signal_one(lock_type& lock) const
899 {
900 686818x state_ |= signaled_bit;
901 686818x bool have_waiters = state_ > signaled_bit;
902 686818x lock.unlock();
903 686818x if (have_waiters)
904 4769x cond_.notify_one();
905 686818x return have_waiters;
906 }
907
908 inline void
909 841x reactor_scheduler::clear_signal() const
910 {
911 841x state_ &= ~signaled_bit;
912 841x }
913
914 inline void
915 831x reactor_scheduler::wait_for_signal(lock_type& lock) const
916 {
917 1747x while ((state_ & signaled_bit) == 0)
918 {
919 916x state_ += waiter_increment;
920 916x cond_.wait(lock);
921 916x state_ -= waiter_increment;
922 }
923 831x }
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 39298x reactor_scheduler::wake_one_thread_and_unlock(lock_type& lock) const
938 {
939 39298x if (maybe_unlock_and_signal_one(lock))
940 215x return;
941
942 39083x if (task_running_.load(std::memory_order_relaxed) && !task_interrupted_)
943 {
944 1614x task_interrupted_ = true;
945 // Under the lock, for the same reason as the signal above.
946 1614x interrupt_reactor();
947 1614x lock.unlock();
948 }
949 else
950 {
951 37469x lock.unlock();
952 }
953 }
954
955 423346x inline reactor_scheduler::work_cleanup::~work_cleanup()
956 {
957 423346x std::int64_t produced = ctx.private_outstanding_work;
958 423346x if (produced > 1)
959 1011x sched->outstanding_work_.fetch_add(
960 produced - 1, std::memory_order_relaxed);
961 422335x else if (produced < 1)
962 65834x sched->work_finished();
963 423346x ctx.private_outstanding_work = 0;
964
965 423346x if (!ctx.private_queue.empty())
966 {
967 319757x lock->lock();
968 319757x sched->completed_ops_.splice(ctx.private_queue);
969 }
970 423346x }
971
972 365579x inline reactor_scheduler::task_cleanup::~task_cleanup()
973 {
974 365579x if (ctx.private_outstanding_work > 0)
975 {
976 10592x sched->outstanding_work_.fetch_add(
977 10592x ctx.private_outstanding_work, std::memory_order_relaxed);
978 10592x ctx.private_outstanding_work = 0;
979 }
980
981 365579x if (!ctx.private_queue.empty())
982 {
983 10592x if (!lock->owns_lock())
984 ✗ lock->lock();
985 10592x sched->completed_ops_.splice(ctx.private_queue);
986 }
987 365579x }
988
989 inline std::size_t
990 433643x reactor_scheduler::do_one(lock_type& lock, long timeout_us, context_type& ctx)
991 {
992 for (;;)
993 {
994 797367x if (stopped_.load(std::memory_order_acquire))
995 6470x return 0;
996
997 790897x std::uintptr_t e = completed_ops_.pop();
998 790897x scheduler_op* op = ready_is_continuation(e) ? nullptr : ready_as_op(e);
999
1000 // Handle reactor sentinel — time to poll for I/O
1001 790897x if (op == &task_op_)
1002 {
1003 366710x bool more_handlers = !completed_ops_.empty();
1004
1005 468053x if (!more_handlers &&
1006 202686x (outstanding_work_.load(std::memory_order_acquire) == 0 ||
1007 timeout_us == 0))
1008 {
1009 1131x completed_ops_.push(&task_op_);
1010 1131x return 0;
1011 }
1012
1013 365579x long task_timeout_us = more_handlers ? 0 : timeout_us;
1014 365579x task_interrupted_ = task_timeout_us == 0;
1015 365579x 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 365579x if (more_handlers && !one_thread_)
1020 265356x unlock_and_signal_one(lock);
1021
1022 try
1023 {
1024 365579x 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 365576x task_running_.store(false, std::memory_order_relaxed);
1033 365576x completed_ops_.push(&task_op_);
1034 365576x settle_retired(lock);
1035 365576x if (timeout_us > 0)
1036 2693x return 0;
1037 362883x continue;
1038 362883x }
1039
1040 // Handle ready entry (op or continuation)
1041 424187x if (e != 0)
1042 {
1043 423346x bool more = !completed_ops_.empty();
1044
1045 423346x if (more && !one_thread_)
1046 {
1047 // Wake a peer for the remaining work; unassisted if none
1048 // was parked to take it.
1049 421462x ctx.unassisted = !unlock_and_signal_one(lock);
1050 }
1051 else
1052 {
1053 // No peer to wake (one_thread_, or nothing more queued).
1054 1884x ctx.unassisted = more;
1055 1884x lock.unlock();
1056 }
1057
1058 423346x [[maybe_unused]] work_cleanup on_exit{this, &lock, ctx};
1059
1060 423346x if (ready_is_continuation(e))
1061 245053x ready_as_cont(e)->h.resume();
1062 else
1063 178293x (*op)();
1064 423346x return 1;
1065 423346x }
1066
1067 1682x if (outstanding_work_.load(std::memory_order_acquire) == 0 ||
1068 timeout_us == 0)
1069 ✗ return 0;
1070
1071 841x clear_signal();
1072 841x if (timeout_us < 0)
1073 831x wait_for_signal(lock);
1074 else
1075 10x wait_for_signal_for(lock, timeout_us);
1076 363724x }
1077 }
1078
1079 } // namespace boost::corosio::detail
1080
1081 #endif // BOOST_COROSIO_NATIVE_DETAIL_REACTOR_REACTOR_SCHEDULER_HPP
1082