Skip to content

Commit 747ddd2

Browse files
committed
Add thread-local private queue to bypass mutex contention
When posting work from within the scheduler's run loop, use a thread-local queue instead of acquiring the global mutex. This matches Asio's thread_info::private_op_queue optimization. - Extend scheduler_context with private_queue and work counter - Fast path in post() detects same-thread via context_stack - Drain points: before blocking, after reactor splice, on exit - Reduces futex calls from ~450K to 1 in multi-threaded benchmarks
1 parent 07db5a1 commit 747ddd2

2 files changed

Lines changed: 90 additions & 3 deletions

File tree

src/corosio/src/detail/epoll/scheduler.cpp

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,15 @@ struct scheduler_context
9292
{
9393
epoll_scheduler const* key;
9494
scheduler_context* next;
95+
op_queue private_queue;
96+
long private_outstanding_work;
97+
98+
scheduler_context(epoll_scheduler const* k, scheduler_context* n)
99+
: key(k)
100+
, next(n)
101+
, private_outstanding_work(0)
102+
{
103+
}
95104
};
96105

97106
corosio::detail::thread_local_ptr<scheduler_context> context_stack;
@@ -102,17 +111,28 @@ struct thread_context_guard
102111

103112
explicit thread_context_guard(
104113
epoll_scheduler const* ctx) noexcept
105-
: frame_{ctx, context_stack.get()}
114+
: frame_(ctx, context_stack.get())
106115
{
107116
context_stack.set(&frame_);
108117
}
109118

110119
~thread_context_guard() noexcept
111120
{
121+
if (!frame_.private_queue.empty())
122+
frame_.key->drain_thread_queue(frame_.private_queue, frame_.private_outstanding_work);
112123
context_stack.set(frame_.next);
113124
}
114125
};
115126

127+
scheduler_context*
128+
find_context(epoll_scheduler const* self) noexcept
129+
{
130+
for (auto* c = context_stack.get(); c != nullptr; c = c->next)
131+
if (c->key == self)
132+
return c;
133+
return nullptr;
134+
}
135+
116136
} // namespace
117137

118138
epoll_scheduler::
@@ -259,6 +279,17 @@ post(capy::coro h) const
259279
};
260280

261281
auto ph = std::make_unique<post_handler>(h);
282+
283+
// Fast path: same thread posts to private queue without locking
284+
if (auto* ctx = find_context(this))
285+
{
286+
outstanding_work_.fetch_add(1, std::memory_order_relaxed);
287+
++ctx->private_outstanding_work;
288+
ctx->private_queue.push(ph.release());
289+
return;
290+
}
291+
292+
// Slow path: cross-thread post requires mutex
262293
outstanding_work_.fetch_add(1, std::memory_order_relaxed);
263294

264295
std::unique_lock lock(mutex_);
@@ -270,6 +301,16 @@ void
270301
epoll_scheduler::
271302
post(scheduler_op* h) const
272303
{
304+
// Fast path: same thread posts to private queue without locking
305+
if (auto* ctx = find_context(this))
306+
{
307+
outstanding_work_.fetch_add(1, std::memory_order_relaxed);
308+
++ctx->private_outstanding_work;
309+
ctx->private_queue.push(h);
310+
return;
311+
}
312+
313+
// Slow path: cross-thread post requires mutex
273314
outstanding_work_.fetch_add(1, std::memory_order_relaxed);
274315

275316
std::unique_lock lock(mutex_);
@@ -489,6 +530,17 @@ work_finished() const noexcept
489530
}
490531
}
491532

533+
void
534+
epoll_scheduler::
535+
drain_thread_queue(op_queue& queue, long count) const
536+
{
537+
std::lock_guard lock(mutex_);
538+
// Note: outstanding_work_ was already incremented when posting
539+
completed_ops_.splice(queue);
540+
if (count > 0)
541+
wakeup_event_.notify_all();
542+
}
543+
492544
void
493545
epoll_scheduler::
494546
interrupt_reactor() const
@@ -548,7 +600,6 @@ update_timerfd() const
548600
if (nearest == timer_service::time_point::max())
549601
{
550602
// No timers - disarm by setting to 0 (relative)
551-
// ts is already zeroed
552603
}
553604
else
554605
{
@@ -754,6 +805,17 @@ run_reactor(std::unique_lock<std::mutex>& lock)
754805
if (!local_ops.empty())
755806
completed_ops_.splice(local_ops);
756807

808+
// Drain private queue (outstanding_work_ was already incremented when posting)
809+
if (auto* ctx = find_context(this))
810+
{
811+
if (!ctx->private_queue.empty())
812+
{
813+
completions_queued += ctx->private_outstanding_work;
814+
ctx->private_outstanding_work = 0;
815+
completed_ops_.splice(ctx->private_queue);
816+
}
817+
}
818+
757819
// Only wake threads that are actually idle, and only as many as we have work
758820
if (completions_queued > 0 && idle_thread_count_ > 0)
759821
{
@@ -778,7 +840,10 @@ do_one(long timeout_us)
778840

779841
if (op == &task_op_)
780842
{
781-
bool more_handlers = !completed_ops_.empty();
843+
// Check both global queue and private queue for pending handlers
844+
auto* ctx = find_context(this);
845+
bool more_handlers = !completed_ops_.empty() ||
846+
(ctx && !ctx->private_queue.empty());
782847

783848
if (!more_handlers)
784849
{
@@ -821,6 +886,17 @@ do_one(long timeout_us)
821886
if (timeout_us == 0)
822887
return 0;
823888

889+
// Drain private queue before blocking (outstanding_work_ was already incremented)
890+
if (auto* ctx = find_context(this))
891+
{
892+
if (!ctx->private_queue.empty())
893+
{
894+
ctx->private_outstanding_work = 0;
895+
completed_ops_.splice(ctx->private_queue);
896+
continue;
897+
}
898+
}
899+
824900
++idle_thread_count_;
825901
if (timeout_us < 0)
826902
wakeup_event_.wait(lock);

src/corosio/src/detail/epoll/scheduler.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class epoll_scheduler
7070
capy::execution_context& ctx,
7171
int concurrency_hint = -1);
7272

73+
/// Destroy the scheduler.
7374
~epoll_scheduler();
7475

7576
epoll_scheduler(epoll_scheduler const&) = delete;
@@ -130,6 +131,16 @@ class epoll_scheduler
130131
/** For use by I/O operations to track completed work. */
131132
void work_finished() const noexcept override;
132133

134+
/** Drain work from thread context's private queue to global queue.
135+
136+
Called by thread_context_guard destructor when a thread exits run().
137+
Transfers pending work to the global queue under mutex protection.
138+
139+
@param queue The private queue to drain.
140+
@param count Item count for wakeup decisions (wakes other threads if positive).
141+
*/
142+
void drain_thread_queue(op_queue& queue, long count) const;
143+
133144
private:
134145
std::size_t do_one(long timeout_us);
135146
void run_reactor(std::unique_lock<std::mutex>& lock);

0 commit comments

Comments
 (0)