Skip to content

Commit b9ddaf7

Browse files
committed
Replace per-operation thread spawning with shared thread pool
1 parent 4fc38ac commit b9ddaf7

6 files changed

Lines changed: 572 additions & 340 deletions

File tree

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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_DETAIL_THREAD_POOL_HPP
11+
#define BOOST_COROSIO_DETAIL_THREAD_POOL_HPP
12+
13+
#include <boost/corosio/detail/config.hpp>
14+
#include <boost/corosio/detail/intrusive.hpp>
15+
#include <boost/capy/ex/execution_context.hpp>
16+
17+
#include <condition_variable>
18+
#include <mutex>
19+
#include <thread>
20+
#include <vector>
21+
22+
namespace boost::corosio::detail {
23+
24+
/** Base class for thread pool work items.
25+
26+
Derive from this to create work that can be posted to a
27+
@ref thread_pool. Uses static function pointer dispatch,
28+
consistent with the IOCP `op` pattern.
29+
30+
@par Example
31+
@code
32+
struct my_work : pool_work_item
33+
{
34+
int* result;
35+
static void execute( pool_work_item* w ) noexcept
36+
{
37+
auto* self = static_cast<my_work*>( w );
38+
*self->result = 42;
39+
}
40+
};
41+
42+
my_work w;
43+
w.func_ = &my_work::execute;
44+
w.result = &r;
45+
pool.post( &w );
46+
@endcode
47+
*/
48+
struct pool_work_item : intrusive_queue<pool_work_item>::node
49+
{
50+
/// Static dispatch function signature.
51+
using func_type = void (*)(pool_work_item*) noexcept;
52+
53+
/// Completion handler invoked by the worker thread.
54+
func_type func_ = nullptr;
55+
};
56+
57+
/** Shared thread pool for dispatching blocking operations.
58+
59+
Provides a fixed pool of reusable worker threads for operations
60+
that cannot be integrated with async I/O (e.g. blocking DNS
61+
calls). Registered as an `execution_context::service` so it
62+
is a singleton per io_context.
63+
64+
Threads are created eagerly in the constructor. The default
65+
thread count is 1.
66+
67+
@par Thread Safety
68+
All public member functions are thread-safe.
69+
70+
@par Shutdown
71+
Sets a shutdown flag, notifies all threads, and joins them.
72+
In-flight blocking calls complete naturally before the thread
73+
exits.
74+
*/
75+
class thread_pool final
76+
: public capy::execution_context::service
77+
{
78+
std::mutex mutex_;
79+
std::condition_variable cv_;
80+
intrusive_queue<pool_work_item> work_queue_;
81+
std::vector<std::thread> threads_;
82+
bool shutdown_ = false;
83+
84+
void worker_loop();
85+
86+
public:
87+
using key_type = thread_pool;
88+
89+
/** Construct the thread pool service.
90+
91+
Eagerly creates all worker threads.
92+
93+
@param ctx Reference to the owning execution_context.
94+
@param num_threads Number of worker threads (default 1).
95+
*/
96+
explicit thread_pool(
97+
capy::execution_context& ctx,
98+
unsigned num_threads = 1)
99+
{
100+
(void)ctx;
101+
threads_.reserve(num_threads);
102+
for (unsigned i = 0; i < num_threads; ++i)
103+
threads_.emplace_back([this] { worker_loop(); });
104+
}
105+
106+
~thread_pool() override = default;
107+
108+
thread_pool(thread_pool const&) = delete;
109+
thread_pool& operator=(thread_pool const&) = delete;
110+
111+
/** Enqueue a work item for execution on the thread pool.
112+
113+
Zero-allocation: the caller owns the work item's storage.
114+
115+
@param w The work item to execute. Must remain valid until
116+
its `func_` has been called.
117+
*/
118+
void post(pool_work_item* w) noexcept;
119+
120+
/** Shut down the thread pool.
121+
122+
Signals all threads to exit after draining any
123+
remaining queued work, then joins them.
124+
*/
125+
void shutdown() override;
126+
};
127+
128+
inline void
129+
thread_pool::worker_loop()
130+
{
131+
for (;;)
132+
{
133+
pool_work_item* w;
134+
{
135+
std::unique_lock<std::mutex> lock(mutex_);
136+
cv_.wait(lock, [this] {
137+
return shutdown_ || !work_queue_.empty();
138+
});
139+
140+
w = work_queue_.pop();
141+
if (!w)
142+
{
143+
if (shutdown_)
144+
return;
145+
continue;
146+
}
147+
}
148+
w->func_(w);
149+
}
150+
}
151+
152+
inline void
153+
thread_pool::post(pool_work_item* w) noexcept
154+
{
155+
{
156+
std::lock_guard<std::mutex> lock(mutex_);
157+
if (shutdown_)
158+
return;
159+
work_queue_.push(w);
160+
}
161+
cv_.notify_one();
162+
}
163+
164+
inline void
165+
thread_pool::shutdown()
166+
{
167+
{
168+
std::lock_guard<std::mutex> lock(mutex_);
169+
shutdown_ = true;
170+
}
171+
cv_.notify_all();
172+
173+
for (auto& t : threads_)
174+
{
175+
if (t.joinable())
176+
t.join();
177+
}
178+
threads_.clear();
179+
180+
{
181+
std::lock_guard<std::mutex> lock(mutex_);
182+
while (work_queue_.pop())
183+
;
184+
}
185+
}
186+
187+
} // namespace boost::corosio::detail
188+
189+
#endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP

include/boost/corosio/native/detail/iocp/win_resolver.hpp

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#endif
2424

2525
#include <boost/corosio/detail/scheduler.hpp>
26+
#include <boost/corosio/detail/thread_pool.hpp>
2627
#include <boost/corosio/endpoint.hpp>
2728
#include <boost/corosio/resolver.hpp>
2829
#include <boost/corosio/resolver_results.hpp>
@@ -42,12 +43,9 @@
4243
#include <WS2tcpip.h>
4344

4445
#include <atomic>
45-
#include <condition_variable>
4646
#include <cstring>
4747
#include <memory>
4848
#include <string>
49-
#include <thread>
50-
#include <unordered_map>
5149

5250
// MinGW may not have GetAddrInfoExCancel declared
5351
#if defined(__MINGW32__) || defined(__MINGW64__)
@@ -72,30 +70,28 @@ extern "C"
7270
Reverse Resolution (GetNameInfoW)
7371
---------------------------------
7472
Unlike GetAddrInfoExW, GetNameInfoW has no async variant. Reverse
75-
resolution spawns a detached worker thread that calls GetNameInfoW
76-
and posts the result to the scheduler upon completion.
73+
resolution dispatches the blocking call to the shared
74+
resolver_thread_pool service.
7775
7876
Class Hierarchy
7977
---------------
8078
- win_resolver_service (execution_context::service)
8179
- Owns all win_resolver instances via shared_ptr
8280
- Coordinates with win_scheduler for work tracking
83-
- Tracks active worker threads for safe shutdown
8481
- win_resolver (one per resolver object)
8582
- Contains embedded resolve_op and reverse_resolve_op
8683
- Inherits from enable_shared_from_this for thread safety
8784
- resolve_op (overlapped_op subclass)
8885
- OVERLAPPED base enables IOCP integration
8986
- Static completion() callback invoked by Windows
9087
- reverse_resolve_op (overlapped_op subclass)
91-
- Used by worker thread for reverse resolution
88+
- Used by pool thread for reverse resolution
9289
93-
Shutdown Synchronization
94-
------------------------
95-
The service uses condition_variable_any and win_mutex to track active
96-
worker threads. During shutdown(), the service waits for all threads
97-
to complete before destroying resources. Worker threads always post
98-
their completions so the scheduler can properly drain them via destroy().
90+
Shutdown
91+
--------
92+
The resolver service cancels all resolvers and clears the impl map.
93+
The thread pool service shuts down separately via execution_context
94+
service ordering, joining all worker threads.
9995
10096
Cancellation
10197
------------
@@ -128,17 +124,14 @@ extern "C"
128124
129125
Reverse Resolution (GetNameInfoW)
130126
---------------------------------
131-
Unlike GetAddrInfoExW, GetNameInfoW has no async variant. We use a worker
132-
thread approach similar to POSIX:
133-
1. reverse_resolve() spawns a detached worker thread
134-
2. Worker calls GetNameInfoW() (blocking)
135-
3. Worker converts wide results to UTF-8 via WideCharToMultiByte
136-
4. Worker posts completion to scheduler
127+
Unlike GetAddrInfoExW, GetNameInfoW has no async variant. The blocking
128+
call is dispatched to the shared resolver_thread_pool:
129+
1. reverse_resolve() posts work to the thread pool
130+
2. Pool thread calls GetNameInfoW() (blocking)
131+
3. Pool thread converts wide results to UTF-8 via WideCharToMultiByte
132+
4. Pool thread posts completion to scheduler
137133
5. op_() resumes the coroutine with results
138134
139-
Thread tracking (thread_started/thread_finished) ensures safe shutdown
140-
by waiting for all worker threads before destroying the service.
141-
142135
String Conversion
143136
-----------------
144137
Windows APIs require wide strings. We use MultiByteToWideChar for
@@ -250,6 +243,16 @@ class win_resolver final
250243
friend struct resolve_op;
251244

252245
public:
246+
/// Embedded pool work item for thread pool dispatch.
247+
struct pool_op : pool_work_item
248+
{
249+
/// Resolver that owns this work item.
250+
win_resolver* resolver_ = nullptr;
251+
252+
/// Prevent impl destruction while work is in flight.
253+
std::shared_ptr<win_resolver> ref_;
254+
};
255+
253256
explicit win_resolver(win_resolver_service& svc) noexcept;
254257

255258
std::coroutine_handle<> resolve(
@@ -276,6 +279,12 @@ class win_resolver final
276279
resolve_op op_;
277280
reverse_resolve_op reverse_op_;
278281

282+
/// Pool work item for reverse resolution.
283+
pool_op reverse_pool_op_;
284+
285+
/// Execute blocking `GetNameInfoW()` on a pool thread.
286+
static void do_reverse_resolve_work(pool_work_item*) noexcept;
287+
279288
private:
280289
win_resolver_service& svc_;
281290
};

0 commit comments

Comments
 (0)