TLA Line data 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 HIT 21233 : void push(continuation* c) noexcept
73 : {
74 21233 : c->reserved = nullptr;
75 21233 : if(tail_)
76 6796 : tail_->reserved = c;
77 : else
78 14437 : head_ = c;
79 21233 : tail_ = c;
80 21233 : }
81 :
82 24041 : continuation* pop() noexcept
83 : {
84 24041 : if(!head_)
85 2808 : return nullptr;
86 21233 : continuation* c = head_;
87 21233 : head_ = static_cast<continuation*>(head_->reserved);
88 21233 : if(!head_)
89 14437 : tail_ = nullptr;
90 21233 : return c;
91 : }
92 :
93 35012 : bool empty() const noexcept
94 : {
95 35012 : 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 1404 : ~impl() = default;
111 :
112 : bool
113 3185 : running_in_this_thread() const noexcept
114 : {
115 3185 : 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 2808 : drain_abandoned() noexcept
123 : {
124 : for(;;)
125 : {
126 : continuation* c;
127 : {
128 3462 : std::lock_guard<std::mutex> lock(mutex_);
129 3462 : c = pop();
130 3462 : }
131 3462 : if(!c)
132 2808 : return;
133 654 : if(c->h && c->h != std::noop_coroutine())
134 403 : capy::abandon(*c);
135 654 : }
136 : }
137 :
138 1404 : impl(std::size_t num_threads, std::string_view thread_name_prefix)
139 1404 : : num_threads_(num_threads)
140 : {
141 1404 : if(num_threads_ == 0)
142 8 : num_threads_ = std::max(
143 4 : std::thread::hardware_concurrency(), 1u);
144 :
145 : // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
146 1404 : auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
147 1404 : thread_name_prefix_[n] = '\0';
148 1404 : }
149 :
150 : void
151 21233 : post(continuation& c)
152 : {
153 21233 : if(!started_.load(std::memory_order_acquire))
154 : {
155 : {
156 574 : std::lock_guard<std::mutex> lock(mutex_);
157 574 : if(stop_)
158 : {
159 : // Stopped before any thread started: nothing will
160 : // run it, and the destructor's drain abandons it.
161 219 : push(&c);
162 219 : return;
163 : }
164 574 : }
165 355 : ensure_started();
166 : }
167 21014 : std::lock_guard<std::mutex> lock(mutex_);
168 21014 : push(&c);
169 : // Under the lock so the pool cannot drain, join, and
170 : // destroy the condition variable mid-signal.
171 21014 : work_cv_.notify_one();
172 21014 : }
173 :
174 : void
175 4062 : on_work_started() noexcept
176 : {
177 4062 : outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
178 4062 : }
179 :
180 : void
181 3762 : on_work_finished() noexcept
182 : {
183 3762 : auto n = outstanding_work_.load(std::memory_order_relaxed);
184 3763 : while(n > 1)
185 : {
186 1262 : if(outstanding_work_.compare_exchange_weak(
187 : n, n - 1, std::memory_order_acq_rel))
188 630 : return;
189 : }
190 3132 : std::lock_guard<std::mutex> lock(mutex_);
191 3132 : outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192 : // Re-read: a concurrent on_work_started() may have raised it.
193 3132 : if(outstanding_work_.load(
194 3132 : std::memory_order_acquire) == 0 && joined_ && !stop_)
195 : {
196 732 : stop_ = true;
197 732 : done_cv_.notify_all();
198 732 : work_cv_.notify_all();
199 : }
200 3132 : }
201 :
202 : void
203 2665 : join() noexcept
204 : {
205 : {
206 2665 : std::unique_lock<std::mutex> lock(mutex_);
207 2665 : if(joined_)
208 1261 : return;
209 1404 : joined_ = true;
210 :
211 1404 : if(outstanding_work_.load(
212 1404 : std::memory_order_acquire) == 0)
213 : {
214 317 : stop_ = true;
215 317 : work_cv_.notify_all();
216 : }
217 : else
218 : {
219 1087 : done_cv_.wait(lock, [this]{
220 2120 : return stop_;
221 : });
222 : }
223 2665 : }
224 :
225 1880 : for(auto& t : threads_)
226 476 : if(t.joinable())
227 476 : t.join();
228 : }
229 :
230 : void
231 1707 : 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 1707 : std::lock_guard<std::mutex> lock(mutex_);
236 1707 : stop_ = true;
237 1707 : work_cv_.notify_all();
238 1707 : done_cv_.notify_all();
239 1707 : }
240 :
241 : private:
242 : void
243 355 : 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 355 : std::lock_guard<std::mutex> lock(mutex_);
249 355 : if(started_.load(std::memory_order_relaxed) || stop_)
250 7 : return;
251 348 : threads_.reserve(num_threads_);
252 824 : for(std::size_t i = 0; i < num_threads_; ++i)
253 952 : threads_.emplace_back([this, i]{ run(i); });
254 348 : started_.store(true, std::memory_order_release);
255 355 : }
256 :
257 : void
258 476 : run(std::size_t index)
259 : {
260 : // Build name; set_current_thread_name truncates to platform limits.
261 : char name[16];
262 476 : std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
263 476 : 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 476 : scoped_pool(impl const* p) noexcept { current_.set(p); }
270 476 : ~scoped_pool() noexcept { current_.set(nullptr); }
271 476 : } guard(this);
272 :
273 : for(;;)
274 : {
275 21055 : continuation* c = nullptr;
276 : {
277 21055 : std::unique_lock<std::mutex> lock(mutex_);
278 21055 : work_cv_.wait(lock, [this]{
279 49347 : return !empty() ||
280 49347 : stop_;
281 : });
282 21055 : if(stop_)
283 952 : return;
284 20579 : c = pop();
285 21055 : }
286 20579 : if(c)
287 20579 : safe_resume(c->h);
288 20579 : }
289 476 : }
290 : };
291 :
292 : //------------------------------------------------------------------------------
293 :
294 1404 : thread_pool::
295 : ~thread_pool()
296 : {
297 1404 : impl_->stop();
298 1404 : impl_->join();
299 1404 : 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 1404 : impl_->drain_abandoned();
303 1404 : destroy();
304 : // Services give up their work in shutdown(), but anything a
305 : // destructor posts anyway is still destroyed.
306 1404 : impl_->drain_abandoned();
307 1404 : delete impl_;
308 1404 : }
309 :
310 1404 : thread_pool::
311 1404 : thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
312 1404 : : impl_(new impl(num_threads, thread_name_prefix))
313 : {
314 1404 : this->set_frame_allocator(std::allocator<void>{});
315 1404 : }
316 :
317 : void
318 1261 : thread_pool::
319 : join() noexcept
320 : {
321 1261 : impl_->join();
322 1261 : }
323 :
324 : void
325 303 : thread_pool::
326 : stop() noexcept
327 : {
328 303 : impl_->stop();
329 303 : }
330 :
331 : //------------------------------------------------------------------------------
332 :
333 : thread_pool::executor_type
334 15321 : thread_pool::
335 : get_executor() const noexcept
336 : {
337 15321 : return executor_type(
338 15321 : const_cast<thread_pool&>(*this));
339 : }
340 :
341 : void
342 4062 : thread_pool::executor_type::
343 : on_work_started() const noexcept
344 : {
345 4062 : pool_->impl_->on_work_started();
346 4062 : }
347 :
348 : void
349 3762 : thread_pool::executor_type::
350 : on_work_finished() const noexcept
351 : {
352 3762 : pool_->impl_->on_work_finished();
353 3762 : }
354 :
355 : void
356 18055 : thread_pool::executor_type::
357 : post(continuation& c) const
358 : {
359 18055 : pool_->impl_->post(c);
360 18055 : }
361 :
362 : std::coroutine_handle<>
363 3185 : thread_pool::executor_type::
364 : dispatch(continuation& c) const
365 : {
366 3185 : if(pool_->impl_->running_in_this_thread())
367 7 : return c.h;
368 3178 : pool_->impl_->post(c);
369 3178 : return std::noop_coroutine();
370 : }
371 :
372 : } // capy
373 : } // boost
|