Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions doc/modules/ROOT/pages/4.guide/4c.io-context.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,17 @@ corosio::io_context ioc;
----

Creates an `io_context` with a concurrency hint of
`std::max(2u, std::thread::hardware_concurrency())` — the default constructor
never selects single-threaded mode. Single-threaded (lockless) mode is keyed
on a `concurrency_hint` of exactly `1`; pass `1` explicitly to opt into it.
`std::max(1u, std::thread::hardware_concurrency())`. The concurrency hint only
tunes performance and never changes the safety contract; single-threaded
(lockless) mode is opt-in exclusively via the `single_threaded_lockless` option
(see xref:4.guide/4c2.configuration.adoc#single-threaded-mode[Single-Threaded Mode]).

=== With Concurrency Hint

[source,cpp]
----
corosio::io_context ioc(1); // Single-threaded, no synchronization
corosio::io_context ioc(4); // Up to 4 threads, thread-safe
corosio::io_context ioc(1); // one-thread hint, still fully thread-safe
corosio::io_context ioc(4); // up to 4 threads
----

The concurrency hint affects:
Expand All @@ -73,7 +74,10 @@ The concurrency hint affects:
concurrently. It is not a library-managed thread pool, and it is distinct
from `thread_pool_size`.

Use `1` for single-threaded programs to avoid synchronization overhead.
The hint never disables locking — a context built with `concurrency_hint == 1`
remains fully thread-safe, so other threads may `post()` into it. To drop
synchronization overhead in a genuinely single-threaded program, set
`io_context_options::single_threaded_lockless = true` instead.

== Running the Event Loop

Expand Down Expand Up @@ -245,8 +249,9 @@ int main()

== Thread Safety

The `io_context` can be used from multiple threads when constructed with
a concurrency hint greater than 1:
The `io_context` is thread-safe by default, regardless of the concurrency hint:
multiple threads may call `run()` concurrently, and any thread may `post()` work
into it.

[source,cpp]
----
Expand All @@ -261,7 +266,9 @@ for (auto& t : threads)
----

Multiple threads can call `run()` concurrently. The `io_context` distributes
work across threads.
work across threads. The one exception is lockless mode: constructing with
`io_context_options::single_threaded_lockless = true` drops these guarantees (see
xref:4.guide/4c2.configuration.adoc#single-threaded-mode[Single-Threaded Mode]).

WARNING: Individual I/O objects (sockets, timers) are not thread-safe.
Don't access the same socket from multiple threads without synchronization.
Expand Down
17 changes: 13 additions & 4 deletions doc/modules/ROOT/pages/4.guide/4c2.configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ corosio::native_io_context<corosio::epoll> ioc(opts);
blocking file I/O and DNS resolution. Ignored on IOCP where
file I/O uses native overlapped I/O.

| `single_threaded`
| `single_threaded_lockless`
| false
| all
| Disable all scheduler mutex and condition variable operations.
Eliminates synchronization overhead when only one thread calls
`run()`. See <<single-threaded-mode>> for restrictions.
`run()`. The only way to enable lockless mode; the concurrency
hint never disables locking. See <<single-threaded-mode>> for
restrictions.
|===

Options that do not apply to the active backend are silently ignored.
Expand Down Expand Up @@ -144,16 +146,23 @@ and DNS resolution use a shared thread pool.
* *No file I/O*: leave at 1 (the pool is created lazily).

[#single-threaded-mode]
=== Single-Threaded Mode (`single_threaded`)
=== Single-Threaded Mode (`single_threaded_lockless`)

Disables all mutex and condition variable operations inside the
scheduler and per-socket descriptor states. This eliminates
15-25% of overhead on the post-and-dispatch hot path.

Setting `single_threaded_lockless = true` is the *only* way to enable
lockless mode. The `concurrency_hint` passed at construction never
disables locking — in particular, `concurrency_hint == 1` keeps the
context fully thread-safe (unlike some other libraries, where a hint
of `1` silently elides synchronization). Enabling this option
transfers responsibility for the restrictions below to the caller.

[source,cpp]
----
corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;

corosio::io_context ioc(opts);
ioc.run(); // only one thread may call this
Expand Down
42 changes: 23 additions & 19 deletions include/boost/corosio/io_context.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,26 +97,33 @@ struct io_context_options
*/
unsigned thread_pool_size = 1;

/** Enable single-threaded mode (disable scheduler locking).
/** Enable single-threaded, lockless mode (disable scheduler locking).

When true, the scheduler skips all mutex lock/unlock and
condition variable operations on the hot path. This
eliminates synchronization overhead when only one thread
calls `run()`.

Enabling this drops the io_context's thread-safety guarantees:
the caller takes on responsibility for the restrictions below.
Leaving it `false` (the default) keeps the context fully
thread-safe.

@par Restrictions
- Only one thread may call `run()` (or any run variant).
- Posting work from another thread is undefined behavior.
- DNS resolution returns `operation_not_supported`.
- POSIX file I/O returns `operation_not_supported`.
- Signal sets should not be shared across contexts.

@note Constructing an `io_context` with `concurrency_hint == 1`
automatically enables single-threaded mode regardless of
this field's value, matching asio's convention. To opt out,
pass `concurrency_hint > 1`.
@note This field is the *only* way to enable lockless mode. The
`concurrency_hint` passed at construction never changes the
safety contract — it only tunes performance. In particular,
`concurrency_hint == 1` keeps full thread safety (unlike
asio's named `ASIO_CONCURRENCY_HINT_UNSAFE` constants, which
this field mirrors).
*/
bool single_threaded = false;
bool single_threaded_lockless = false;

/** Enable IORING_SETUP_SQPOLL on the io_uring backend.

Expand All @@ -126,7 +133,7 @@ struct io_context_options
path. Most useful for sustained traffic. Idle thread parks
after `sq_thread_idle_ms` of no activity.

Independent of `single_threaded`. Default: off.
Independent of `single_threaded_lockless`. Default: off.

Ignored on non-io_uring backends.
*/
Expand Down Expand Up @@ -196,8 +203,10 @@ class timer_service;

@par Thread Safety
Distinct objects: Safe.@n
Shared objects: Safe, if using a concurrency hint greater
than 1.
Shared objects: Safe, unless the context was constructed with
`io_context_options::single_threaded_lockless = true` (lockless
mode), in which case only one thread may use it. The
`concurrency_hint` does not affect thread safety.

@see epoll_t, select_t, kqueue_t, iocp_t
*/
Expand All @@ -211,9 +220,6 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
io_context_options const& opts,
unsigned concurrency_hint);

/// Switch the scheduler to single-threaded (lockless) mode.
void configure_single_threaded_();

protected:
detail::scheduler* sched_;

Expand All @@ -223,11 +229,11 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context

/** Construct with default concurrency and platform backend.

Uses `std::thread::hardware_concurrency()` clamped to a minimum
of 2 as the concurrency hint, so the default constructor never
silently engages single-threaded mode (see
@ref io_context_options::single_threaded). Pass an explicit
`concurrency_hint == 1` to opt into single-threaded mode.
Uses `std::thread::hardware_concurrency()` (floored to 1, in
case it reports 0) as the concurrency hint. The hint only tunes
performance and never affects thread safety; single-threaded
(lockless) mode is opt-in exclusively via
@ref io_context_options::single_threaded_lockless.
*/
io_context();

Expand Down Expand Up @@ -266,8 +272,6 @@ class BOOST_COROSIO_DECL io_context : public capy::execution_context
{
(void)backend;
sched_ = &Backend::construct(*this, concurrency_hint);
if (concurrency_hint == 1)
configure_single_threaded_();
}

/** Construct with an explicit backend tag and runtime options.
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/accept_churn_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ bench_sequential_churn_lockless(bench::state& state)
using acceptor_type = corosio::native_tcp_acceptor<Backend>;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
acceptor_type acc(ioc);
acc.open();
Expand Down Expand Up @@ -397,7 +397,7 @@ bench_burst_churn_lockless(bench::state& state)
state.counters["burst_size"] = burst_size;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
acceptor_type acc(ioc);
acc.open();
Expand Down
6 changes: 3 additions & 3 deletions perf/bench/corosio/fan_out_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ bench_fork_join_lockless(bench::state& state)
state.counters["fan_out"] = fan_out;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);

std::vector<socket_type> clients;
Expand Down Expand Up @@ -413,7 +413,7 @@ bench_nested_lockless(bench::state& state)
state.counters["subs_per_group"] = subs_per_group;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);

std::vector<socket_type> clients;
Expand Down Expand Up @@ -508,7 +508,7 @@ bench_concurrent_parents_lockless(bench::state& state)
state.counters["fan_out"] = fan_out;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);

std::vector<socket_type> clients;
Expand Down
2 changes: 1 addition & 1 deletion perf/bench/corosio/http_server_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ bench_single_connection_lockless(bench::state& state)
using socket_type = corosio::native_tcp_socket<Backend>;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
auto [client, server] = corosio::test::make_socket_pair<
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/io_context_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ void
bench_single_threaded_lockless(bench::state& state)
{
corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;

corosio::native_io_context<Backend> ioc(opts, 1);
auto ex = ioc.get_executor();
Expand Down Expand Up @@ -295,7 +295,7 @@ void
bench_interleaved_lockless(bench::state& state)
{
corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;

int handlers_per_iteration = 100;

Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/local_socket_latency_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ bench_unix_pingpong_latency_lockless(bench::state& state)
state.counters["message_size"] = static_cast<double>(message_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
socket_type client(ioc), server(ioc);
if (auto ec = corosio::connect_pair(client, server))
Expand Down Expand Up @@ -201,7 +201,7 @@ bench_unix_concurrent_latency_lockless(bench::state& state)
state.counters["num_pairs"] = num_pairs;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);

std::vector<socket_type> clients;
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/local_socket_throughput_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ bench_unix_throughput_lockless(bench::state& state)
state.counters["chunk_size"] = static_cast<double>(chunk_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
socket_type writer(ioc), reader(ioc);
if (auto ec = corosio::connect_pair(writer, reader))
Expand Down Expand Up @@ -259,7 +259,7 @@ bench_unix_bidirectional_throughput_lockless(bench::state& state)
state.counters["chunk_size"] = static_cast<double>(chunk_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
socket_type sock1(ioc), sock2(ioc);
if (auto ec = corosio::connect_pair(sock1, sock2))
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/socket_latency_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ bench_pingpong_latency_lockless(bench::state& state)
state.counters["message_size"] = static_cast<double>(message_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
auto [client, server] = corosio::test::make_socket_pair<
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
Expand Down Expand Up @@ -203,7 +203,7 @@ bench_concurrent_latency_lockless(bench::state& state)
state.counters["num_pairs"] = num_pairs;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);

std::vector<socket_type> clients;
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/socket_throughput_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,7 @@ bench_throughput_lockless(bench::state& state)
state.counters["chunk_size"] = static_cast<double>(chunk_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
auto [writer, reader] = corosio::test::make_socket_pair<
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
Expand Down Expand Up @@ -320,7 +320,7 @@ bench_bidirectional_throughput_lockless(bench::state& state)
state.counters["chunk_size"] = static_cast<double>(chunk_size);

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
auto [sock1, sock2] = corosio::test::make_socket_pair<
socket_type, corosio::native_tcp_acceptor<Backend>>(ioc);
Expand Down
4 changes: 2 additions & 2 deletions perf/bench/corosio/timer_bench.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ bench_schedule_cancel_lockless(bench::state& state)
using timer_type = corosio::native_timer<Backend>;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
int64_t counter = 0;
int constexpr batch_size = 1000;
Expand Down Expand Up @@ -147,7 +147,7 @@ bench_fire_rate_lockless(bench::state& state)
using timer_type = corosio::native_timer<Backend>;

corosio::io_context_options opts;
opts.single_threaded = true;
opts.single_threaded_lockless = true;
corosio::native_io_context<Backend> ioc(opts, 1);
std::atomic<bool> running{true};
int64_t counter = 0;
Expand Down
Loading
Loading