Skip to content

Commit cc79129

Browse files
committed
Replace per-operation thread spawning with shared thread pool
Resolver operations previously created and detached a new std::thread for every blocking getaddrinfo()/getnameinfo() call. Add a generic detail::thread_pool execution_context service that reuses threads across operations, and wire both POSIX and Windows resolver services to dispatch blocking work through it.
1 parent 4fc38ac commit cc79129

6 files changed

Lines changed: 402 additions & 320 deletions

File tree

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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/capy/ex/execution_context.hpp>
15+
16+
#include <condition_variable>
17+
#include <deque>
18+
#include <functional>
19+
#include <mutex>
20+
#include <thread>
21+
#include <vector>
22+
23+
namespace boost::corosio::detail {
24+
25+
/** Shared thread pool for dispatching blocking operations.
26+
27+
Provides a fixed pool of reusable worker threads for operations
28+
that cannot be integrated with async I/O (e.g. blocking DNS
29+
calls). Registered as an `execution_context::service` so it
30+
is a singleton per io_context.
31+
32+
Threads are created lazily on the first `post()` call. The
33+
default thread count is 1.
34+
35+
@par Thread Safety
36+
All public member functions are thread-safe.
37+
38+
@par Shutdown
39+
Sets a shutdown flag, notifies all threads, and joins them.
40+
In-flight blocking calls complete naturally before the thread
41+
exits.
42+
*/
43+
class thread_pool final
44+
: public capy::execution_context::service
45+
{
46+
std::mutex mutex_;
47+
std::condition_variable cv_;
48+
std::deque<std::function<void()>> work_queue_;
49+
std::vector<std::thread> threads_;
50+
bool shutdown_ = false;
51+
bool started_ = false;
52+
unsigned num_threads_;
53+
54+
void start_threads();
55+
void worker_loop();
56+
57+
public:
58+
using key_type = thread_pool;
59+
60+
/** Construct the thread pool service.
61+
62+
@param ctx Reference to the owning execution_context.
63+
@param num_threads Number of worker threads (default 1).
64+
*/
65+
explicit thread_pool(
66+
capy::execution_context& ctx,
67+
unsigned num_threads = 1)
68+
: num_threads_(num_threads)
69+
{
70+
(void)ctx;
71+
}
72+
73+
~thread_pool() override = default;
74+
75+
thread_pool(thread_pool const&) = delete;
76+
thread_pool& operator=(thread_pool const&) = delete;
77+
78+
/** Enqueue a work item for execution on the thread pool.
79+
80+
Starts threads lazily on the first call.
81+
82+
@param f The callable to execute.
83+
*/
84+
void post(std::function<void()> f);
85+
86+
/** Shut down the thread pool.
87+
88+
Signals all threads to exit after draining any
89+
remaining queued work, then joins them.
90+
*/
91+
void shutdown() override;
92+
};
93+
94+
inline void
95+
thread_pool::start_threads()
96+
{
97+
threads_.reserve(num_threads_);
98+
for (unsigned i = 0; i < num_threads_; ++i)
99+
threads_.emplace_back([this] { worker_loop(); });
100+
started_ = true;
101+
}
102+
103+
inline void
104+
thread_pool::worker_loop()
105+
{
106+
for (;;)
107+
{
108+
std::function<void()> task;
109+
{
110+
std::unique_lock<std::mutex> lock(mutex_);
111+
cv_.wait(lock, [this] {
112+
return shutdown_ || !work_queue_.empty();
113+
});
114+
115+
if (work_queue_.empty())
116+
{
117+
if (shutdown_)
118+
return;
119+
continue;
120+
}
121+
122+
task = std::move(work_queue_.front());
123+
work_queue_.pop_front();
124+
}
125+
task();
126+
}
127+
}
128+
129+
inline void
130+
thread_pool::post(std::function<void()> f)
131+
{
132+
{
133+
std::lock_guard<std::mutex> lock(mutex_);
134+
if (!started_)
135+
start_threads();
136+
work_queue_.push_back(std::move(f));
137+
}
138+
cv_.notify_one();
139+
}
140+
141+
inline void
142+
thread_pool::shutdown()
143+
{
144+
{
145+
std::lock_guard<std::mutex> lock(mutex_);
146+
shutdown_ = true;
147+
}
148+
cv_.notify_all();
149+
150+
for (auto& t : threads_)
151+
{
152+
if (t.joinable())
153+
t.join();
154+
}
155+
threads_.clear();
156+
}
157+
158+
} // namespace boost::corosio::detail
159+
160+
#endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP

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

Lines changed: 14 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,9 @@
4242
#include <WS2tcpip.h>
4343

4444
#include <atomic>
45-
#include <condition_variable>
4645
#include <cstring>
4746
#include <memory>
4847
#include <string>
49-
#include <thread>
50-
#include <unordered_map>
5148

5249
// MinGW may not have GetAddrInfoExCancel declared
5350
#if defined(__MINGW32__) || defined(__MINGW64__)
@@ -72,30 +69,28 @@ extern "C"
7269
Reverse Resolution (GetNameInfoW)
7370
---------------------------------
7471
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.
72+
resolution dispatches the blocking call to the shared
73+
resolver_thread_pool service.
7774
7875
Class Hierarchy
7976
---------------
8077
- win_resolver_service (execution_context::service)
8178
- Owns all win_resolver instances via shared_ptr
8279
- Coordinates with win_scheduler for work tracking
83-
- Tracks active worker threads for safe shutdown
8480
- win_resolver (one per resolver object)
8581
- Contains embedded resolve_op and reverse_resolve_op
8682
- Inherits from enable_shared_from_this for thread safety
8783
- resolve_op (overlapped_op subclass)
8884
- OVERLAPPED base enables IOCP integration
8985
- Static completion() callback invoked by Windows
9086
- reverse_resolve_op (overlapped_op subclass)
91-
- Used by worker thread for reverse resolution
87+
- Used by pool thread for reverse resolution
9288
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().
89+
Shutdown
90+
--------
91+
The resolver service cancels all resolvers and clears the impl map.
92+
The thread pool service shuts down separately via execution_context
93+
service ordering, joining all worker threads.
9994
10095
Cancellation
10196
------------
@@ -128,17 +123,14 @@ extern "C"
128123
129124
Reverse Resolution (GetNameInfoW)
130125
---------------------------------
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
126+
Unlike GetAddrInfoExW, GetNameInfoW has no async variant. The blocking
127+
call is dispatched to the shared resolver_thread_pool:
128+
1. reverse_resolve() posts work to the thread pool
129+
2. Pool thread calls GetNameInfoW() (blocking)
130+
3. Pool thread converts wide results to UTF-8 via WideCharToMultiByte
131+
4. Pool thread posts completion to scheduler
137132
5. op_() resumes the coroutine with results
138133
139-
Thread tracking (thread_started/thread_finished) ensures safe shutdown
140-
by waiting for all worker threads before destroying the service.
141-
142134
String Conversion
143135
-----------------
144136
Windows APIs require wide strings. We use MultiByteToWideChar for

0 commit comments

Comments
 (0)