src/ex/thread_pool.cpp

99.3% Lines (150 / 151) 100.0% Functions (28 / 28)
thread_pool.cpp
f(x) Functions (28)
Function Calls Lines Blocks
boost::capy::thread_pool::impl::push(boost::capy::continuation*) :72 22835x 100.0% 100.0% boost::capy::thread_pool::impl::pop() :82 25699x 100.0% 100.0% boost::capy::thread_pool::impl::empty() const :93 40822x 100.0% 100.0% boost::capy::thread_pool::impl::~impl() :110 1432x 100.0% 100.0% boost::capy::thread_pool::impl::running_in_this_thread() const :113 3210x 100.0% 100.0% boost::capy::thread_pool::impl::drain_abandoned() :122 2864x 100.0% 100.0% boost::capy::thread_pool::impl::impl(unsigned long, std::basic_string_view<char, std::char_traits<char> >) :138 1432x 100.0% 72.0% boost::capy::thread_pool::impl::post(boost::capy::continuation&) :151 22837x 100.0% 95.0% boost::capy::thread_pool::impl::on_work_started() :175 4090x 100.0% 100.0% boost::capy::thread_pool::impl::on_work_finished() :181 3790x 100.0% 75.0% boost::capy::thread_pool::impl::join() :203 2725x 100.0% 85.0% boost::capy::thread_pool::impl::join()::{lambda()#1}::operator()() const :219 2067x 100.0% 100.0% boost::capy::thread_pool::impl::stop() :231 1735x 100.0% 100.0% boost::capy::thread_pool::impl::ensure_started() :243 377x 83.3% 88.0% boost::capy::thread_pool::impl::ensure_started()::{lambda()#1}::operator()() const :255 499x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long) :260 499x 100.0% 78.0% boost::capy::thread_pool::impl::run(unsigned long)::scoped_pool::scoped_pool(boost::capy::thread_pool::impl const*) :271 499x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long)::scoped_pool::~scoped_pool() :272 499x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long)::{lambda()#1}::operator()() const :280 40822x 100.0% 100.0% boost::capy::thread_pool::~thread_pool() :296 1432x 100.0% 100.0% boost::capy::thread_pool::thread_pool(unsigned long, std::basic_string_view<char, std::char_traits<char> >) :312 1432x 100.0% 55.0% boost::capy::thread_pool::join() :320 1293x 100.0% 100.0% boost::capy::thread_pool::stop() :327 303x 100.0% 100.0% boost::capy::thread_pool::get_executor() const :336 15351x 100.0% 100.0% boost::capy::thread_pool::executor_type::on_work_started() const :344 4090x 100.0% 100.0% boost::capy::thread_pool::executor_type::on_work_finished() const :351 3790x 100.0% 100.0% boost::capy::thread_pool::executor_type::post(boost::capy::continuation&) const :358 19631x 100.0% 100.0% boost::capy::thread_pool::executor_type::dispatch(boost::capy::continuation&) const :365 3210x 100.0% 100.0%
Line TLA Hits Source Code
1 //
2 // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3 // Copyright (c) 2026 Michael Vandeberg
4 //
5 // Distributed under the Boost Software License, Version 1.0. (See accompanying
6 // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7 //
8 // Official repository: https://github.com/boostorg/capy
9 //
10
11 #include <boost/capy/ex/thread_pool.hpp>
12 #include <boost/capy/continuation.hpp>
13 #include <boost/capy/detail/thread_local_ptr.hpp>
14 #include <boost/capy/ex/abandon.hpp>
15 #include <boost/capy/ex/frame_allocator.hpp>
16 #include <boost/capy/test/thread_name.hpp>
17 #include <algorithm>
18 #include <atomic>
19 #include <condition_variable>
20 #include <cstdio>
21 #include <mutex>
22 #include <thread>
23 #include <vector>
24
25 /*
26 Thread pool implementation using a shared work queue.
27
28 Work items are continuations linked via their intrusive next pointer,
29 stored in a single queue protected by a mutex. No per-post heap
30 allocation: the continuation is owned by the caller and linked
31 directly. Worker threads wait on a condition_variable until work
32 is available or stop is requested.
33
34 Threads are started lazily on first post(), under mutex_, to avoid
35 spawning threads for pools that are constructed but never used. A
36 stopped pool starts none. Each thread is named
37 with a configurable prefix plus index for debugger visibility.
38
39 Work tracking: on_work_started/on_work_finished maintain the atomic
40 outstanding_work_ counter. on_work_started is lock-free, and so is
41 on_work_finished while the count stays above one. The release that
42 may take it to zero is made under mutex_, so join() can't see zero,
43 return and let the pool be destroyed while that call still uses it.
44 join() blocks until this counter reaches zero, then signals workers
45 to stop and joins threads.
46
47 Two shutdown paths:
48 - join(): waits for outstanding work to drain, then stops workers.
49 - stop(): immediately signals workers to exit; queued work stays queued.
50 - Destructor: stop() then join(), then queued work is abandoned.
51 */
52
53 namespace boost {
54 namespace capy {
55
56 //------------------------------------------------------------------------------
57
58 class thread_pool::impl
59 {
60 // Identifies the pool owning the current worker thread, or
61 // nullptr if the calling thread is not a pool worker. Checked
62 // by dispatch() to decide between symmetric transfer (inline
63 // resume) and post.
64 static inline detail::thread_local_ptr<impl const> current_;
65
66 // Intrusive queue of continuations: the next link is stored in
67 // continuation::reserved (typed continuation* round-tripped through
68 // void*). No per-post allocation: the continuation is owned by the caller.
69 continuation* head_ = nullptr;
70 continuation* tail_ = nullptr;
71
72 22835x void push(continuation* c) noexcept
73 {
74 22835x c->reserved = nullptr;
75 22835x if(tail_)
76 4887x tail_->reserved = c;
77 else
78 17948x head_ = c;
79 22835x tail_ = c;
80 22835x }
81
82 25699x continuation* pop() noexcept
83 {
84 25699x if(!head_)
85 2864x return nullptr;
86 22835x continuation* c = head_;
87 22835x head_ = static_cast<continuation*>(head_->reserved);
88 22835x if(!head_)
89 17948x tail_ = nullptr;
90 22835x return c;
91 }
92
93 40822x bool empty() const noexcept
94 {
95 40822x return head_ == nullptr;
96 }
97
98 std::mutex mutex_;
99 std::condition_variable work_cv_;
100 std::condition_variable done_cv_;
101 std::vector<std::thread> threads_;
102 std::atomic<std::size_t> outstanding_work_{0};
103 bool stop_{false};
104 bool joined_{false};
105 std::size_t num_threads_;
106 char thread_name_prefix_[13]{}; // 12 chars max + null terminator
107 std::atomic<bool> started_{false};
108
109 public:
110 1432x ~impl() = default;
111
112 bool
113 3210x running_in_this_thread() const noexcept
114 {
115 3210x return current_.get() == this;
116 }
117
118 // Abandon queued coroutines. Each is taken under the lock, as other
119 // threads may still post, and abandoned outside it, as abandoning
120 // can post.
121 void
122 2864x drain_abandoned() noexcept
123 {
124 for(;;)
125 {
126 continuation* c;
127 {
128 3395x std::lock_guard<std::mutex> lock(mutex_);
129 3395x c = pop();
130 3395x }
131 3395x if(!c)
132 2864x return;
133 531x if(c->h && c->h != std::noop_coroutine())
134 278x capy::abandon(*c);
135 531x }
136 }
137
138 1432x impl(std::size_t num_threads, std::string_view thread_name_prefix)
139 1432x : num_threads_(num_threads)
140 {
141 1432x if(num_threads_ == 0)
142 8x num_threads_ = std::max(
143 4x std::thread::hardware_concurrency(), 1u);
144
145 // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
146 1432x auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
147 1432x thread_name_prefix_[n] = '\0';
148 1432x }
149
150 void
151 22837x post(continuation& c)
152 {
153 22837x if(!started_.load(std::memory_order_acquire))
154 {
155 {
156 597x std::lock_guard<std::mutex> lock(mutex_);
157 597x if(stop_)
158 {
159 // Stopped before any thread started: nothing will
160 // run it, and the destructor's drain abandons it.
161 220x push(&c);
162 220x return;
163 }
164 597x }
165 377x ensure_started();
166 }
167 22615x std::lock_guard<std::mutex> lock(mutex_);
168 22615x push(&c);
169 // Under the lock so the pool cannot drain, join, and
170 // destroy the condition variable mid-signal.
171 22615x work_cv_.notify_one();
172 22615x }
173
174 void
175 4090x on_work_started() noexcept
176 {
177 4090x outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
178 4090x }
179
180 void
181 3790x on_work_finished() noexcept
182 {
183 3790x auto n = outstanding_work_.load(std::memory_order_relaxed);
184 3796x while(n > 1)
185 {
186 2702x if(outstanding_work_.compare_exchange_weak(
187 n, n - 1, std::memory_order_acq_rel))
188 1345x return;
189 }
190 2445x std::lock_guard<std::mutex> lock(mutex_);
191 2445x outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192 // Re-read: a concurrent on_work_started() may have raised it.
193 2445x if(outstanding_work_.load(
194 2445x std::memory_order_acquire) == 0 && joined_ && !stop_)
195 {
196 705x stop_ = true;
197 705x done_cv_.notify_all();
198 705x work_cv_.notify_all();
199 }
200 2445x }
201
202 void
203 2725x join() noexcept
204 {
205 {
206 2725x std::unique_lock<std::mutex> lock(mutex_);
207 2725x if(joined_)
208 1293x return;
209 1432x joined_ = true;
210
211 1432x if(outstanding_work_.load(
212 1432x std::memory_order_acquire) == 0)
213 {
214 371x stop_ = true;
215 371x work_cv_.notify_all();
216 }
217 else
218 {
219 1061x done_cv_.wait(lock, [this]{
220 2067x return stop_;
221 });
222 }
223 2725x }
224
225 1931x for(auto& t : threads_)
226 499x if(t.joinable())
227 499x t.join();
228 }
229
230 void
231 1735x stop() noexcept
232 {
233 // Under the lock: the join() this releases may return and let
234 // the pool be destroyed as soon as the lock is free.
235 1735x std::lock_guard<std::mutex> lock(mutex_);
236 1735x stop_ = true;
237 1735x work_cv_.notify_all();
238 1735x done_cv_.notify_all();
239 1735x }
240
241 private:
242 void
243 377x ensure_started()
244 {
245 // Under the lock, so join() either sees every thread started
246 // here or makes this start none. A joined pool still starts them:
247 // join() may be waiting on work that needs them.
248 377x std::lock_guard<std::mutex> lock(mutex_);
249 377x if(started_.load(std::memory_order_relaxed) || stop_)
250 ✗ return;
251 // Start at the threads already running: if an earlier call
252 // threw partway, this one adds only the missing ones.
253 377x threads_.reserve(num_threads_);
254 876x for(std::size_t i = threads_.size(); i < num_threads_; ++i)
255 1000x threads_.emplace_back([this, i]{ run(i); });
256 375x started_.store(true, std::memory_order_release);
257 377x }
258
259 void
260 499x run(std::size_t index)
261 {
262 // Build name; set_current_thread_name truncates to platform limits.
263 char name[16];
264 499x std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
265 499x set_current_thread_name(name);
266
267 // Mark this thread as a worker of this pool so dispatch()
268 // can symmetric-transfer when called from within pool work.
269 struct scoped_pool
270 {
271 499x scoped_pool(impl const* p) noexcept { current_.set(p); }
272 499x ~scoped_pool() noexcept { current_.set(nullptr); }
273 499x } guard(this);
274
275 for(;;)
276 {
277 22803x continuation* c = nullptr;
278 {
279 22803x std::unique_lock<std::mutex> lock(mutex_);
280 22803x work_cv_.wait(lock, [this]{
281 59241x return !empty() ||
282 59241x stop_;
283 });
284 22803x if(stop_)
285 998x return;
286 22304x c = pop();
287 22803x }
288 22304x if(c)
289 22304x safe_resume(c->h);
290 22304x }
291 499x }
292 };
293
294 //------------------------------------------------------------------------------
295
296 1432x thread_pool::
297 ~thread_pool()
298 {
299 1432x impl_->stop();
300 1432x impl_->join();
301 1432x shutdown();
302 // Takes what the unwinds post too, and the work of any service they
303 // create, which is shut down as it is created.
304 1432x impl_->drain_abandoned();
305 1432x destroy();
306 // Services give up their work in shutdown(), but anything a
307 // destructor posts anyway is still destroyed.
308 1432x impl_->drain_abandoned();
309 1432x delete impl_;
310 1432x }
311
312 1432x thread_pool::
313 1432x thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
314 1432x : impl_(new impl(num_threads, thread_name_prefix))
315 {
316 1432x this->set_frame_allocator(std::allocator<void>{});
317 1432x }
318
319 void
320 1293x thread_pool::
321 join() noexcept
322 {
323 1293x impl_->join();
324 1293x }
325
326 void
327 303x thread_pool::
328 stop() noexcept
329 {
330 303x impl_->stop();
331 303x }
332
333 //------------------------------------------------------------------------------
334
335 thread_pool::executor_type
336 15351x thread_pool::
337 get_executor() const noexcept
338 {
339 15351x return executor_type(
340 15351x const_cast<thread_pool&>(*this));
341 }
342
343 void
344 4090x thread_pool::executor_type::
345 on_work_started() const noexcept
346 {
347 4090x pool_->impl_->on_work_started();
348 4090x }
349
350 void
351 3790x thread_pool::executor_type::
352 on_work_finished() const noexcept
353 {
354 3790x pool_->impl_->on_work_finished();
355 3790x }
356
357 void
358 19631x thread_pool::executor_type::
359 post(continuation& c) const
360 {
361 19631x pool_->impl_->post(c);
362 19629x }
363
364 std::coroutine_handle<>
365 3210x thread_pool::executor_type::
366 dispatch(continuation& c) const
367 {
368 3210x if(pool_->impl_->running_in_this_thread())
369 4x return c.h;
370 3206x pool_->impl_->post(c);
371 3206x return std::noop_coroutine();
372 }
373
374 } // capy
375 } // boost
376