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 21851x 100.0% 100.0% boost::capy::thread_pool::impl::pop() :82 24719x 100.0% 100.0% boost::capy::thread_pool::impl::empty() const :93 40477x 100.0% 100.0% boost::capy::thread_pool::impl::~impl() :110 1434x 100.0% 100.0% boost::capy::thread_pool::impl::running_in_this_thread() const :113 3217x 100.0% 100.0% boost::capy::thread_pool::impl::drain_abandoned() :122 2868x 100.0% 100.0% boost::capy::thread_pool::impl::impl(unsigned long, std::basic_string_view<char, std::char_traits<char> >) :138 1434x 100.0% 72.0% boost::capy::thread_pool::impl::post(boost::capy::continuation&) :151 21851x 100.0% 95.0% boost::capy::thread_pool::impl::on_work_started() :175 4094x 100.0% 100.0% boost::capy::thread_pool::impl::on_work_finished() :181 3794x 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 1737x 100.0% 100.0% boost::capy::thread_pool::impl::ensure_started() :243 377x 83.3% 76.0% boost::capy::thread_pool::impl::ensure_started()::{lambda()#1}::operator()() const :253 502x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long) :258 502x 100.0% 78.0% boost::capy::thread_pool::impl::run(unsigned long)::scoped_pool::scoped_pool(boost::capy::thread_pool::impl const*) :269 502x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long)::scoped_pool::~scoped_pool() :270 502x 100.0% 100.0% boost::capy::thread_pool::impl::run(unsigned long)::{lambda()#1}::operator()() const :278 40477x 100.0% 100.0% boost::capy::thread_pool::~thread_pool() :294 1434x 100.0% 100.0% boost::capy::thread_pool::thread_pool(unsigned long, std::basic_string_view<char, std::char_traits<char> >) :310 1434x 100.0% 55.0% boost::capy::thread_pool::join() :318 1291x 100.0% 100.0% boost::capy::thread_pool::stop() :325 303x 100.0% 100.0% boost::capy::thread_pool::get_executor() const :334 15351x 100.0% 100.0% boost::capy::thread_pool::executor_type::on_work_started() const :342 4094x 100.0% 100.0% boost::capy::thread_pool::executor_type::on_work_finished() const :349 3794x 100.0% 100.0% boost::capy::thread_pool::executor_type::post(boost::capy::continuation&) const :356 18641x 100.0% 100.0% boost::capy::thread_pool::executor_type::dispatch(boost::capy::continuation&) const :363 3217x 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 21851x void push(continuation* c) noexcept
73 {
74 21851x c->reserved = nullptr;
75 21851x if(tail_)
76 3261x tail_->reserved = c;
77 else
78 18590x head_ = c;
79 21851x tail_ = c;
80 21851x }
81
82 24719x continuation* pop() noexcept
83 {
84 24719x if(!head_)
85 2868x return nullptr;
86 21851x continuation* c = head_;
87 21851x head_ = static_cast<continuation*>(head_->reserved);
88 21851x if(!head_)
89 18590x tail_ = nullptr;
90 21851x return c;
91 }
92
93 40477x bool empty() const noexcept
94 {
95 40477x 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 1434x ~impl() = default;
111
112 bool
113 3217x running_in_this_thread() const noexcept
114 {
115 3217x 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 2868x drain_abandoned() noexcept
123 {
124 for(;;)
125 {
126 continuation* c;
127 {
128 3399x std::lock_guard<std::mutex> lock(mutex_);
129 3399x c = pop();
130 3399x }
131 3399x if(!c)
132 2868x return;
133 531x if(c->h && c->h != std::noop_coroutine())
134 280x capy::abandon(*c);
135 531x }
136 }
137
138 1434x impl(std::size_t num_threads, std::string_view thread_name_prefix)
139 1434x : num_threads_(num_threads)
140 {
141 1434x 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 1434x auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
147 1434x thread_name_prefix_[n] = '\0';
148 1434x }
149
150 void
151 21851x post(continuation& c)
152 {
153 21851x 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 21631x std::lock_guard<std::mutex> lock(mutex_);
168 21631x push(&c);
169 // Under the lock so the pool cannot drain, join, and
170 // destroy the condition variable mid-signal.
171 21631x work_cv_.notify_one();
172 21631x }
173
174 void
175 4094x on_work_started() noexcept
176 {
177 4094x outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
178 4094x }
179
180 void
181 3794x on_work_finished() noexcept
182 {
183 3794x auto n = outstanding_work_.load(std::memory_order_relaxed);
184 3799x while(n > 1)
185 {
186 2236x if(outstanding_work_.compare_exchange_weak(
187 n, n - 1, std::memory_order_acq_rel))
188 1113x return;
189 }
190 2681x std::lock_guard<std::mutex> lock(mutex_);
191 2681x outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192 // Re-read: a concurrent on_work_started() may have raised it.
193 2681x if(outstanding_work_.load(
194 2681x 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 2681x }
201
202 void
203 2725x join() noexcept
204 {
205 {
206 2725x std::unique_lock<std::mutex> lock(mutex_);
207 2725x if(joined_)
208 1291x return;
209 1434x joined_ = true;
210
211 1434x if(outstanding_work_.load(
212 1434x std::memory_order_acquire) == 0)
213 {
214 373x stop_ = true;
215 373x 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 1936x for(auto& t : threads_)
226 502x if(t.joinable())
227 502x t.join();
228 }
229
230 void
231 1737x 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 1737x std::lock_guard<std::mutex> lock(mutex_);
236 1737x stop_ = true;
237 1737x work_cv_.notify_all();
238 1737x done_cv_.notify_all();
239 1737x }
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 377x threads_.reserve(num_threads_);
252 879x for(std::size_t i = 0; i < num_threads_; ++i)
253 1004x threads_.emplace_back([this, i]{ run(i); });
254 377x started_.store(true, std::memory_order_release);
255 377x }
256
257 void
258 502x run(std::size_t index)
259 {
260 // Build name; set_current_thread_name truncates to platform limits.
261 char name[16];
262 502x std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
263 502x set_current_thread_name(name);
264
265 // Mark this thread as a worker of this pool so dispatch()
266 // can symmetric-transfer when called from within pool work.
267 struct scoped_pool
268 {
269 502x scoped_pool(impl const* p) noexcept { current_.set(p); }
270 502x ~scoped_pool() noexcept { current_.set(nullptr); }
271 502x } guard(this);
272
273 for(;;)
274 {
275 21822x continuation* c = nullptr;
276 {
277 21822x std::unique_lock<std::mutex> lock(mutex_);
278 21822x work_cv_.wait(lock, [this]{
279 59547x return !empty() ||
280 59547x stop_;
281 });
282 21822x if(stop_)
283 1004x return;
284 21320x c = pop();
285 21822x }
286 21320x if(c)
287 21320x safe_resume(c->h);
288 21320x }
289 502x }
290 };
291
292 //------------------------------------------------------------------------------
293
294 1434x thread_pool::
295 ~thread_pool()
296 {
297 1434x impl_->stop();
298 1434x impl_->join();
299 1434x shutdown();
300 // Takes what the unwinds post too, and the work of any service they
301 // create, which is shut down as it is created.
302 1434x impl_->drain_abandoned();
303 1434x destroy();
304 // Services give up their work in shutdown(), but anything a
305 // destructor posts anyway is still destroyed.
306 1434x impl_->drain_abandoned();
307 1434x delete impl_;
308 1434x }
309
310 1434x thread_pool::
311 1434x thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
312 1434x : impl_(new impl(num_threads, thread_name_prefix))
313 {
314 1434x this->set_frame_allocator(std::allocator<void>{});
315 1434x }
316
317 void
318 1291x thread_pool::
319 join() noexcept
320 {
321 1291x impl_->join();
322 1291x }
323
324 void
325 303x thread_pool::
326 stop() noexcept
327 {
328 303x impl_->stop();
329 303x }
330
331 //------------------------------------------------------------------------------
332
333 thread_pool::executor_type
334 15351x thread_pool::
335 get_executor() const noexcept
336 {
337 15351x return executor_type(
338 15351x const_cast<thread_pool&>(*this));
339 }
340
341 void
342 4094x thread_pool::executor_type::
343 on_work_started() const noexcept
344 {
345 4094x pool_->impl_->on_work_started();
346 4094x }
347
348 void
349 3794x thread_pool::executor_type::
350 on_work_finished() const noexcept
351 {
352 3794x pool_->impl_->on_work_finished();
353 3794x }
354
355 void
356 18641x thread_pool::executor_type::
357 post(continuation& c) const
358 {
359 18641x pool_->impl_->post(c);
360 18641x }
361
362 std::coroutine_handle<>
363 3217x thread_pool::executor_type::
364 dispatch(continuation& c) const
365 {
366 3217x if(pool_->impl_->running_in_this_thread())
367 7x return c.h;
368 3210x pool_->impl_->post(c);
369 3210x return std::noop_coroutine();
370 }
371
372 } // capy
373 } // boost
374