Skip to content

Commit 051cea7

Browse files
committed
Add io_context_options for runtime scheduler and service tuning
Introduce io_context_options with seven configurable knobs: max_events_per_poll, inline_budget_initial/max, unassisted_budget, gqcs_timeout_ms, thread_pool_size, and single_threaded. The single_threaded option disables all scheduler and descriptor mutex/condvar operations via conditionally_enabled_mutex/event wrappers, following Asio's model. Cross-thread post is UB when enabled; DNS and file I/O return operation_not_supported. Benchmarks show 2x throughput on the single-threaded post path with zero regression on multi-threaded paths.
1 parent 83ebd10 commit 051cea7

23 files changed

+1236
-59
lines changed

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ corosio::native_io_context<corosio::epoll> ioc(opts);
7474
| Number of worker threads in the shared thread pool used for
7575
blocking file I/O and DNS resolution. Ignored on IOCP where
7676
file I/O uses native overlapped I/O.
77+
78+
| `single_threaded`
79+
| false
80+
| all
81+
| Disable all scheduler mutex and condition variable operations.
82+
Eliminates synchronization overhead when only one thread calls
83+
`run()`. See <<single-threaded-mode>> for restrictions.
7784
|===
7885

7986
Options that do not apply to the active backend are silently ignored.
@@ -120,3 +127,31 @@ and DNS resolution use a shared thread pool.
120127
* *Concurrent file operations*: increase to match expected
121128
parallelism (e.g. 4 for four concurrent file reads).
122129
* *No file I/O*: leave at 1 (the pool is created lazily).
130+
131+
[#single-threaded-mode]
132+
=== Single-Threaded Mode (`single_threaded`)
133+
134+
Disables all mutex and condition variable operations inside the
135+
scheduler and per-socket descriptor states. This eliminates
136+
15-25% of overhead on the post-and-dispatch hot path.
137+
138+
[source,cpp]
139+
----
140+
corosio::io_context_options opts;
141+
opts.single_threaded = true;
142+
143+
corosio::io_context ioc(opts);
144+
ioc.run(); // only one thread may call this
145+
----
146+
147+
WARNING: Single-threaded mode imposes hard restrictions.
148+
Violating them is undefined behavior.
149+
150+
* Only **one thread** may call `run()` (or any run/poll variant).
151+
* **Posting work from another thread** is undefined behavior.
152+
* **DNS resolution** returns `operation_not_supported`.
153+
* **POSIX file I/O** (`stream_file`, `random_access_file`) returns
154+
`operation_not_supported` on `open()`.
155+
* **Signal sets** should not be shared across contexts.
156+
* **Timer cancellation via `stop_token`** from another thread
157+
remains safe (the timer service retains its own mutex).
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// Copyright (c) 2026 Michael Vandeberg
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_DETAIL_CONDITIONALLY_ENABLED_EVENT_HPP
11+
#define BOOST_COROSIO_DETAIL_CONDITIONALLY_ENABLED_EVENT_HPP
12+
13+
#include <boost/corosio/detail/conditionally_enabled_mutex.hpp>
14+
15+
#include <chrono>
16+
#include <condition_variable>
17+
18+
namespace boost::corosio::detail {
19+
20+
/* Condition variable wrapper that becomes a no-op when disabled.
21+
22+
When enabled, notify/wait delegate to an underlying
23+
std::condition_variable. When disabled, all operations
24+
are no-ops. The wait paths are unreachable in
25+
single-threaded mode because the task sentinel prevents
26+
the empty-queue state in do_one().
27+
*/
28+
class conditionally_enabled_event
29+
{
30+
std::condition_variable cond_;
31+
bool enabled_;
32+
33+
public:
34+
explicit conditionally_enabled_event(bool enabled = true) noexcept
35+
: enabled_(enabled)
36+
{
37+
}
38+
39+
conditionally_enabled_event(conditionally_enabled_event const&) = delete;
40+
conditionally_enabled_event& operator=(conditionally_enabled_event const&) = delete;
41+
42+
void set_enabled(bool v) noexcept
43+
{
44+
enabled_ = v;
45+
}
46+
47+
void notify_one()
48+
{
49+
if (enabled_)
50+
cond_.notify_one();
51+
}
52+
53+
void notify_all()
54+
{
55+
if (enabled_)
56+
cond_.notify_all();
57+
}
58+
59+
void wait(conditionally_enabled_mutex::scoped_lock& lock)
60+
{
61+
if (enabled_)
62+
cond_.wait(lock.underlying());
63+
}
64+
65+
template<class Rep, class Period>
66+
void wait_for(
67+
conditionally_enabled_mutex::scoped_lock& lock,
68+
std::chrono::duration<Rep, Period> const& d)
69+
{
70+
if (enabled_)
71+
cond_.wait_for(lock.underlying(), d);
72+
}
73+
};
74+
75+
} // namespace boost::corosio::detail
76+
77+
#endif // BOOST_COROSIO_DETAIL_CONDITIONALLY_ENABLED_EVENT_HPP
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
//
2+
// Copyright (c) 2026 Michael Vandeberg
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_DETAIL_CONDITIONALLY_ENABLED_MUTEX_HPP
11+
#define BOOST_COROSIO_DETAIL_CONDITIONALLY_ENABLED_MUTEX_HPP
12+
13+
#include <mutex>
14+
15+
namespace boost::corosio::detail {
16+
17+
/* Mutex wrapper that becomes a no-op when disabled.
18+
19+
When enabled (the default), lock/unlock delegate to an
20+
underlying std::mutex. When disabled, all operations are
21+
no-ops. The enabled flag is fixed after construction.
22+
23+
scoped_lock wraps std::unique_lock<std::mutex> internally
24+
so that condvar wait paths (which require the real lock
25+
type) compile and work in multi-threaded mode.
26+
*/
27+
class conditionally_enabled_mutex
28+
{
29+
std::mutex mutex_;
30+
bool enabled_;
31+
32+
public:
33+
explicit conditionally_enabled_mutex(bool enabled = true) noexcept
34+
: enabled_(enabled)
35+
{
36+
}
37+
38+
conditionally_enabled_mutex(conditionally_enabled_mutex const&) = delete;
39+
conditionally_enabled_mutex& operator=(conditionally_enabled_mutex const&) = delete;
40+
41+
bool enabled() const noexcept
42+
{
43+
return enabled_;
44+
}
45+
46+
void set_enabled(bool v) noexcept
47+
{
48+
enabled_ = v;
49+
}
50+
51+
// Lockable interface — allows std::lock_guard<conditionally_enabled_mutex>
52+
void lock() { if (enabled_) mutex_.lock(); }
53+
void unlock() { if (enabled_) mutex_.unlock(); }
54+
bool try_lock() { return !enabled_ || mutex_.try_lock(); }
55+
56+
class scoped_lock
57+
{
58+
std::unique_lock<std::mutex> lock_;
59+
bool enabled_;
60+
61+
public:
62+
explicit scoped_lock(conditionally_enabled_mutex& m)
63+
: lock_(m.mutex_, std::defer_lock)
64+
, enabled_(m.enabled_)
65+
{
66+
if (enabled_)
67+
lock_.lock();
68+
}
69+
70+
scoped_lock(scoped_lock const&) = delete;
71+
scoped_lock& operator=(scoped_lock const&) = delete;
72+
73+
void lock()
74+
{
75+
if (enabled_)
76+
lock_.lock();
77+
}
78+
79+
void unlock()
80+
{
81+
if (enabled_)
82+
lock_.unlock();
83+
}
84+
85+
bool owns_lock() const noexcept
86+
{
87+
return enabled_ && lock_.owns_lock();
88+
}
89+
90+
// Access the underlying unique_lock for condvar wait().
91+
// Only called when locking is enabled.
92+
std::unique_lock<std::mutex>& underlying() noexcept
93+
{
94+
return lock_;
95+
}
96+
};
97+
};
98+
99+
} // namespace boost::corosio::detail
100+
101+
#endif // BOOST_COROSIO_DETAIL_CONDITIONALLY_ENABLED_MUTEX_HPP

include/boost/corosio/io_context.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,22 @@ struct io_context_options
9898
where file I/O uses native overlapped I/O.
9999
*/
100100
unsigned thread_pool_size = 1;
101+
102+
/** Enable single-threaded mode (disable scheduler locking).
103+
104+
When true, the scheduler skips all mutex lock/unlock and
105+
condition variable operations on the hot path. This
106+
eliminates synchronization overhead when only one thread
107+
calls `run()`.
108+
109+
@par Restrictions
110+
- Only one thread may call `run()` (or any run variant).
111+
- Posting work from another thread is undefined behavior.
112+
- DNS resolution returns `operation_not_supported`.
113+
- POSIX file I/O returns `operation_not_supported`.
114+
- Signal sets should not be shared across contexts.
115+
*/
116+
bool single_threaded = false;
101117
};
102118

103119
namespace detail {

include/boost/corosio/native/detail/epoll/epoll_scheduler.hpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ class BOOST_COROSIO_DECL epoll_scheduler final : public reactor_scheduler_base
125125

126126
private:
127127
void
128-
run_task(std::unique_lock<std::mutex>& lock, context_type* ctx) override;
128+
run_task(lock_type& lock, context_type* ctx) override;
129129
void interrupt_reactor() const override;
130130
void update_timerfd() const;
131131

@@ -255,9 +255,10 @@ epoll_scheduler::register_descriptor(int fd, descriptor_state* desc) const
255255
desc->registered_events = ev.events;
256256
desc->fd = fd;
257257
desc->scheduler_ = this;
258+
desc->mutex.set_enabled(!single_threaded_);
258259
desc->ready_events_.store(0, std::memory_order_relaxed);
259260

260-
std::lock_guard lock(desc->mutex);
261+
conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
261262
desc->impl_ref_.reset();
262263
desc->read_ready = false;
263264
desc->write_ready = false;
@@ -319,7 +320,7 @@ epoll_scheduler::update_timerfd() const
319320
}
320321

321322
inline void
322-
epoll_scheduler::run_task(std::unique_lock<std::mutex>& lock, context_type* ctx)
323+
epoll_scheduler::run_task(lock_type& lock, context_type* ctx)
323324
{
324325
int timeout_ms = task_interrupted_ ? 0 : -1;
325326

include/boost/corosio/native/detail/kqueue/kqueue_scheduler.hpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class BOOST_COROSIO_DECL kqueue_scheduler final : public reactor_scheduler_base
147147

148148
private:
149149
void
150-
run_task(std::unique_lock<std::mutex>& lock, context_type* ctx) override;
150+
run_task(lock_type& lock, context_type* ctx) override;
151151
void interrupt_reactor() const override;
152152
long calculate_timeout(long requested_timeout_us) const;
153153

@@ -242,9 +242,10 @@ kqueue_scheduler::register_descriptor(int fd, descriptor_state* desc) const
242242
desc->registered_events = kqueue_event_read | kqueue_event_write;
243243
desc->fd = fd;
244244
desc->scheduler_ = this;
245+
desc->mutex.set_enabled(!single_threaded_);
245246
desc->ready_events_.store(0, std::memory_order_relaxed);
246247

247-
std::lock_guard lock(desc->mutex);
248+
conditionally_enabled_mutex::scoped_lock lock(desc->mutex);
248249
desc->impl_ref_.reset();
249250
desc->read_ready = false;
250251
desc->write_ready = false;
@@ -309,7 +310,7 @@ kqueue_scheduler::calculate_timeout(long requested_timeout_us) const
309310

310311
inline void
311312
kqueue_scheduler::run_task(
312-
std::unique_lock<std::mutex>& lock, context_type* ctx)
313+
lock_type& lock, context_type* ctx)
313314
{
314315
long effective_timeout_us = task_interrupted_ ? 0 : calculate_timeout(-1);
315316

include/boost/corosio/native/detail/posix/posix_random_access_file_service.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#if BOOST_COROSIO_POSIX
1616

1717
#include <boost/corosio/native/detail/posix/posix_random_access_file.hpp>
18+
#include <boost/corosio/native/native_scheduler.hpp>
1819
#include <boost/corosio/detail/random_access_file_service.hpp>
1920
#include <boost/corosio/detail/thread_pool.hpp>
2021

@@ -33,6 +34,8 @@ class BOOST_COROSIO_DECL posix_random_access_file_service final
3334
capy::execution_context& ctx, scheduler& sched)
3435
: sched_(&sched)
3536
, pool_(get_or_create_pool(ctx))
37+
, single_threaded_(
38+
static_cast<native_scheduler&>(sched).single_threaded_)
3639
{
3740
}
3841

@@ -80,6 +83,8 @@ class BOOST_COROSIO_DECL posix_random_access_file_service final
8083
std::filesystem::path const& path,
8184
file_base::flags mode) override
8285
{
86+
if (single_threaded_)
87+
return std::make_error_code(std::errc::operation_not_supported);
8388
return static_cast<posix_random_access_file&>(impl).open_file(
8489
path, mode);
8590
}
@@ -134,6 +139,7 @@ class BOOST_COROSIO_DECL posix_random_access_file_service final
134139

135140
scheduler* sched_;
136141
thread_pool& pool_;
142+
bool single_threaded_;
137143
std::mutex mutex_;
138144
intrusive_list<posix_random_access_file> file_list_;
139145
std::unordered_map<

0 commit comments

Comments
 (0)