Skip to content

Commit b002c0e

Browse files
committed
feat(io_context): concurrency_hint heuristic and single_threaded tie-up
Two related improvements to io_context construction: 1. Static heuristic for inline budget defaults: when concurrency_hint > 1 and the user has not customized budget options, override (init, max, unassisted) to (0, 0, 0). Multi-thread workloads benefit from cross-thread work-stealing, which "post everything" enables; single-thread keeps the (2, 16, 4) chaining defaults. 2. Tie concurrency_hint == 1 to single_threaded mode (asio precedent): constructing an io_context with concurrency_hint == 1 now forces opts.single_threaded = true, eliminating scheduler locking on the hot path. Matches asio's one_thread_ behavior. To opt out, pass concurrency_hint > 1. Supporting changes: - reactor_scheduler: short-circuit try_consume_inline_budget and reset_inline_budget when inline_budget_max_ == 0 so post-everything mode skips TLS-list walks; relax configure_reactor validation to allow budget_max == 0; fix max(1, x) * 2 in ramp-up to escape zero. - io_context: apply_options_pre_/post_ now take concurrency_hint; new private configure_single_threaded_() helper used by the bare-hint and template no-opts constructors. - docs: update 4c2.configuration.adoc and io_context_options field docs to describe the queue-empty gating and new defaults.
1 parent a58ffdb commit b002c0e

13 files changed

Lines changed: 141 additions & 35 deletions

‎doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,19 @@ a re-queue through the scheduler.
110110
* *Single-threaded contexts*:
111111
`unassisted_budget` caps the budget when only one thread is
112112
running the event loop, preserving fairness.
113+
* *Disable the fast path entirely*: set all three options to 0 to
114+
force a re-queue on every completion (useful as a baseline or
115+
when a workload is dominated by cross-thread work-stealing).
116+
117+
[NOTE]
118+
====
119+
When `io_context` is constructed with `concurrency_hint > 1` and all
120+
three budget fields are at their defaults `(2, 16, 4)`, the
121+
constructor overrides them to `(0, 0, 0)`. Multi-thread workloads
122+
benefit from cross-thread work-stealing, which "post-everything"
123+
mode enables. Setting any budget field to a non-default value
124+
disables the override.
125+
====
113126

114127
=== IOCP Timeout (`gqcs_timeout_ms`)
115128

‎include/boost/corosio/io_context.hpp‎

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ struct io_context_options
6464
After a posted handler executes, the reactor grants this
6565
many speculative inline completions before forcing a
6666
re-queue. Applies to reactor backends only.
67+
68+
@note Constructing an `io_context` with `concurrency_hint > 1`
69+
and all three budget fields at their defaults overrides
70+
them to disable inline completion (post-everything mode),
71+
since multi-thread workloads benefit from cross-thread
72+
work-stealing. Setting any budget field to a non-default
73+
value disables the override.
6774
*/
6875
unsigned inline_budget_initial = 2;
6976

@@ -112,6 +119,11 @@ struct io_context_options
112119
- DNS resolution returns `operation_not_supported`.
113120
- POSIX file I/O returns `operation_not_supported`.
114121
- Signal sets should not be shared across contexts.
122+
123+
@note Constructing an `io_context` with `concurrency_hint == 1`
124+
automatically enables single-threaded mode regardless of
125+
this field's value, matching asio's convention. To opt out,
126+
pass `concurrency_hint > 1`.
115127
*/
116128
bool single_threaded = false;
117129
};
@@ -158,7 +170,12 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
158170
void apply_options_pre_(io_context_options const& opts);
159171

160172
/// Apply runtime tuning to the scheduler (after construct).
161-
void apply_options_post_(io_context_options const& opts);
173+
void apply_options_post_(
174+
io_context_options const& opts,
175+
unsigned concurrency_hint);
176+
177+
/// Switch the scheduler to single-threaded (lockless) mode.
178+
void configure_single_threaded_();
162179

163180
protected:
164181
detail::timer_service* timer_svc_ = nullptr;
@@ -168,7 +185,14 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
168185
/** The executor type for this context. */
169186
class executor_type;
170187

171-
/** Construct with default concurrency and platform backend. */
188+
/** Construct with default concurrency and platform backend.
189+
190+
Uses `std::thread::hardware_concurrency()` clamped to a minimum
191+
of 2 as the concurrency hint, so the default constructor never
192+
silently engages single-threaded mode (see
193+
@ref io_context_options::single_threaded). Pass an explicit
194+
`concurrency_hint == 1` to opt into single-threaded mode.
195+
*/
172196
io_context();
173197

174198
/** Construct with a concurrency hint and platform backend.
@@ -206,6 +230,8 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
206230
{
207231
(void)backend;
208232
sched_ = &Backend::construct(*this, concurrency_hint);
233+
if (concurrency_hint == 1)
234+
configure_single_threaded_();
209235
}
210236

211237
/** Construct with an explicit backend tag and runtime options.
@@ -229,7 +255,7 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
229255
(void)backend;
230256
apply_options_pre_(opts);
231257
sched_ = &Backend::construct(*this, concurrency_hint);
232-
apply_options_post_(opts);
258+
apply_options_post_(opts, concurrency_hint);
233259
}
234260

235261
~io_context();

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -413,10 +413,9 @@ reactor_scheduler::configure_reactor(
413413
max_events > static_cast<unsigned>(std::numeric_limits<int>::max()))
414414
throw std::out_of_range(
415415
"max_events_per_poll must be in [1, INT_MAX]");
416-
if (budget_max < 1 ||
417-
budget_max > static_cast<unsigned>(std::numeric_limits<int>::max()))
416+
if (budget_max > static_cast<unsigned>(std::numeric_limits<int>::max()))
418417
throw std::out_of_range(
419-
"inline_budget_max must be in [1, INT_MAX]");
418+
"inline_budget_max must be in [0, INT_MAX]");
420419

421420
// Clamp initial and unassisted to budget_max.
422421
if (budget_init > budget_max)
@@ -433,6 +432,10 @@ reactor_scheduler::configure_reactor(
433432
inline void
434433
reactor_scheduler::reset_inline_budget() const noexcept
435434
{
435+
// When budget is disabled (max==0), all paths below would no-op
436+
// (inline_budget stays 0). Skip the TLS lookup entirely.
437+
if (inline_budget_max_ == 0)
438+
return;
436439
if (auto* ctx = reactor_find_context(this))
437440
{
438441
// Cap when no other thread absorbed queued work
@@ -444,10 +447,11 @@ reactor_scheduler::reset_inline_budget() const noexcept
444447
static_cast<int>(unassisted_budget_);
445448
return;
446449
}
447-
// Ramp up when previous cycle fully consumed budget
450+
// Ramp up when previous cycle fully consumed budget.
451+
// max(1, ...) ensures the doubling escapes zero.
448452
if (ctx->inline_budget == 0)
449453
ctx->inline_budget_max = (std::min)(
450-
ctx->inline_budget_max * 2,
454+
(std::max)(1, ctx->inline_budget_max) * 2,
451455
static_cast<int>(inline_budget_max_));
452456
else if (ctx->inline_budget < ctx->inline_budget_max)
453457
ctx->inline_budget_max =
@@ -459,6 +463,8 @@ reactor_scheduler::reset_inline_budget() const noexcept
459463
inline bool
460464
reactor_scheduler::try_consume_inline_budget() const noexcept
461465
{
466+
if (inline_budget_max_ == 0)
467+
return false;
462468
if (auto* ctx = reactor_find_context(this))
463469
{
464470
if (ctx->inline_budget > 0)

‎perf/bench/corosio/accept_churn_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ bench_sequential_churn_lockless(bench::state& state)
133133

134134
corosio::io_context_options opts;
135135
opts.single_threaded = true;
136-
corosio::native_io_context<Backend> ioc(opts);
136+
corosio::native_io_context<Backend> ioc(opts, 1);
137137
acceptor_type acc(ioc);
138138
acc.open();
139139
acc.set_option(corosio::native_socket_option::reuse_address(true));
@@ -398,7 +398,7 @@ bench_burst_churn_lockless(bench::state& state)
398398

399399
corosio::io_context_options opts;
400400
opts.single_threaded = true;
401-
corosio::native_io_context<Backend> ioc(opts);
401+
corosio::native_io_context<Backend> ioc(opts, 1);
402402
acceptor_type acc(ioc);
403403
acc.open();
404404
acc.set_option(corosio::native_socket_option::reuse_address(true));

‎perf/bench/corosio/fan_out_bench.cpp‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ bench_fork_join_lockless(bench::state& state)
337337

338338
corosio::io_context_options opts;
339339
opts.single_threaded = true;
340-
corosio::native_io_context<Backend> ioc(opts);
340+
corosio::native_io_context<Backend> ioc(opts, 1);
341341

342342
std::vector<socket_type> clients;
343343
std::vector<socket_type> servers;
@@ -414,7 +414,7 @@ bench_nested_lockless(bench::state& state)
414414

415415
corosio::io_context_options opts;
416416
opts.single_threaded = true;
417-
corosio::native_io_context<Backend> ioc(opts);
417+
corosio::native_io_context<Backend> ioc(opts, 1);
418418

419419
std::vector<socket_type> clients;
420420
std::vector<socket_type> servers;
@@ -509,7 +509,7 @@ bench_concurrent_parents_lockless(bench::state& state)
509509

510510
corosio::io_context_options opts;
511511
opts.single_threaded = true;
512-
corosio::native_io_context<Backend> ioc(opts);
512+
corosio::native_io_context<Backend> ioc(opts, 1);
513513

514514
std::vector<socket_type> clients;
515515
std::vector<socket_type> servers;

‎perf/bench/corosio/http_server_bench.cpp‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ bench_single_connection_lockless(bench::state& state)
156156

157157
corosio::io_context_options opts;
158158
opts.single_threaded = true;
159-
corosio::native_io_context<Backend> ioc(opts);
159+
corosio::native_io_context<Backend> ioc(opts, 1);
160160
auto [client, server] = corosio::test::make_socket_pair<
161161
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
162162

‎perf/bench/corosio/io_context_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ bench_single_threaded_lockless(bench::state& state)
265265
corosio::io_context_options opts;
266266
opts.single_threaded = true;
267267

268-
corosio::native_io_context<Backend> ioc(opts);
268+
corosio::native_io_context<Backend> ioc(opts, 1);
269269
auto ex = ioc.get_executor();
270270
int64_t counter = 0;
271271
int constexpr batch_size = 1000;
@@ -299,7 +299,7 @@ bench_interleaved_lockless(bench::state& state)
299299

300300
int handlers_per_iteration = 100;
301301

302-
corosio::native_io_context<Backend> ioc(opts);
302+
corosio::native_io_context<Backend> ioc(opts, 1);
303303
auto ex = ioc.get_executor();
304304
int64_t counter = 0;
305305

‎perf/bench/corosio/local_socket_latency_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ bench_unix_pingpong_latency_lockless(bench::state& state)
155155

156156
corosio::io_context_options opts;
157157
opts.single_threaded = true;
158-
corosio::native_io_context<Backend> ioc(opts);
158+
corosio::native_io_context<Backend> ioc(opts, 1);
159159
auto [client, server] = corosio::make_local_stream_pair(ioc);
160160

161161
capy::run_async(ioc.get_executor())(
@@ -185,7 +185,7 @@ bench_unix_concurrent_latency_lockless(bench::state& state)
185185

186186
corosio::io_context_options opts;
187187
opts.single_threaded = true;
188-
corosio::native_io_context<Backend> ioc(opts);
188+
corosio::native_io_context<Backend> ioc(opts, 1);
189189

190190
std::vector<corosio::local_stream_socket> clients;
191191
std::vector<corosio::local_stream_socket> servers;

‎perf/bench/corosio/local_socket_throughput_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ bench_unix_throughput_lockless(bench::state& state)
183183

184184
corosio::io_context_options opts;
185185
opts.single_threaded = true;
186-
corosio::native_io_context<Backend> ioc(opts);
186+
corosio::native_io_context<Backend> ioc(opts, 1);
187187
auto [writer, reader] = corosio::make_local_stream_pair(ioc);
188188

189189
std::vector<char> write_buf(chunk_size, 'x');
@@ -243,7 +243,7 @@ bench_unix_bidirectional_throughput_lockless(bench::state& state)
243243

244244
corosio::io_context_options opts;
245245
opts.single_threaded = true;
246-
corosio::native_io_context<Backend> ioc(opts);
246+
corosio::native_io_context<Backend> ioc(opts, 1);
247247
auto [sock1, sock2] = corosio::make_local_stream_pair(ioc);
248248

249249
std::vector<char> buf1(chunk_size, 'a');

‎perf/bench/corosio/socket_latency_bench.cpp‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ bench_pingpong_latency_lockless(bench::state& state)
168168

169169
corosio::io_context_options opts;
170170
opts.single_threaded = true;
171-
corosio::native_io_context<Backend> ioc(opts);
171+
corosio::native_io_context<Backend> ioc(opts, 1);
172172
auto [client, server] = corosio::test::make_socket_pair<
173173
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
174174

@@ -204,7 +204,7 @@ bench_concurrent_latency_lockless(bench::state& state)
204204

205205
corosio::io_context_options opts;
206206
opts.single_threaded = true;
207-
corosio::native_io_context<Backend> ioc(opts);
207+
corosio::native_io_context<Backend> ioc(opts, 1);
208208

209209
std::vector<socket_type> clients;
210210
std::vector<socket_type> servers;

0 commit comments

Comments
 (0)