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 21967 : void push(continuation* c) noexcept
73 : {
74 21967 : c->reserved = nullptr;
75 21967 : if(tail_)
76 2157 : tail_->reserved = c;
77 : else
78 19810 : head_ = c;
79 21967 : tail_ = c;
80 21967 : }
81 :
82 24817 : continuation* pop() noexcept
83 : {
84 24817 : if(!head_)
85 2850 : return nullptr;
86 21967 : continuation* c = head_;
87 21967 : head_ = static_cast<continuation*>(head_->reserved);
88 21967 : if(!head_)
89 19810 : tail_ = nullptr;
90 21967 : return c;
91 : }
92 :
93 41711 : bool empty() const noexcept
94 : {
95 41711 : 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 1425 : ~impl() = default;
111 :
112 : bool
113 3206 : running_in_this_thread() const noexcept
114 : {
115 3206 : 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 2850 : drain_abandoned() noexcept
123 : {
124 : for(;;)
125 : {
126 : continuation* c;
127 : {
128 3383 : std::lock_guard<std::mutex> lock(mutex_);
129 3383 : c = pop();
130 3383 : }
131 3383 : if(!c)
132 2850 : return;
133 533 : if(c->h && c->h != std::noop_coroutine())
134 244 : capy::abandon(*c);
135 533 : }
136 : }
137 :
138 1425 : impl(std::size_t num_threads, std::string_view thread_name_prefix)
139 1425 : : num_threads_(num_threads)
140 : {
141 1425 : 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 1425 : auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
147 1425 : thread_name_prefix_[n] = '\0';
148 1425 : }
149 :
150 : void
151 21969 : post(continuation& c)
152 : {
153 21969 : if(!started_.load(std::memory_order_acquire))
154 : {
155 : {
156 590 : std::lock_guard<std::mutex> lock(mutex_);
157 590 : if(stop_)
158 : {
159 : // Stopped before any thread started: nothing will
160 : // run it, and the destructor's drain abandons it.
161 220 : push(&c);
162 220 : return;
163 : }
164 590 : }
165 370 : ensure_started();
166 : }
167 21747 : std::lock_guard<std::mutex> lock(mutex_);
168 21747 : push(&c);
169 : // Under the lock so the pool cannot drain, join, and
170 : // destroy the condition variable mid-signal.
171 21747 : work_cv_.notify_one();
172 21747 : }
173 :
174 : void
175 4083 : on_work_started() noexcept
176 : {
177 4083 : outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
178 4083 : }
179 :
180 : void
181 3783 : on_work_finished() noexcept
182 : {
183 3783 : auto n = outstanding_work_.load(std::memory_order_relaxed);
184 3791 : while(n > 1)
185 : {
186 2540 : if(outstanding_work_.compare_exchange_weak(
187 : n, n - 1, std::memory_order_acq_rel))
188 1262 : return;
189 : }
190 2521 : std::lock_guard<std::mutex> lock(mutex_);
191 2521 : outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192 : // Re-read: a concurrent on_work_started() may have raised it.
193 2521 : if(outstanding_work_.load(
194 2521 : std::memory_order_acquire) == 0 && joined_ && !stop_)
195 : {
196 683 : stop_ = true;
197 683 : done_cv_.notify_all();
198 683 : work_cv_.notify_all();
199 : }
200 2521 : }
201 :
202 : void
203 2707 : join() noexcept
204 : {
205 : {
206 2707 : std::unique_lock<std::mutex> lock(mutex_);
207 2707 : if(joined_)
208 1282 : return;
209 1425 : joined_ = true;
210 :
211 1425 : if(outstanding_work_.load(
212 1425 : std::memory_order_acquire) == 0)
213 : {
214 386 : stop_ = true;
215 386 : work_cv_.notify_all();
216 : }
217 : else
218 : {
219 1039 : done_cv_.wait(lock, [this]{
220 2023 : return stop_;
221 : });
222 : }
223 2707 : }
224 :
225 1920 : for(auto& t : threads_)
226 495 : if(t.joinable())
227 495 : t.join();
228 : }
229 :
230 : void
231 1728 : 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 1728 : std::lock_guard<std::mutex> lock(mutex_);
236 1728 : stop_ = true;
237 1728 : work_cv_.notify_all();
238 1728 : done_cv_.notify_all();
239 1728 : }
240 :
241 : private:
242 : void
243 370 : 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 370 : std::lock_guard<std::mutex> lock(mutex_);
249 370 : if(started_.load(std::memory_order_relaxed) || stop_)
250 MIS 0 : return;
251 : // Start at the threads already running: if an earlier call
252 : // threw partway, this one adds only the missing ones.
253 HIT 370 : threads_.reserve(num_threads_);
254 865 : for(std::size_t i = threads_.size(); i < num_threads_; ++i)
255 992 : threads_.emplace_back([this, i]{ run(i); });
256 368 : started_.store(true, std::memory_order_release);
257 370 : }
258 :
259 : void
260 495 : run(std::size_t index)
261 : {
262 : // Build name; set_current_thread_name truncates to platform limits.
263 : char name[16];
264 495 : std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
265 495 : 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 495 : scoped_pool(impl const* p) noexcept { current_.set(p); }
272 495 : ~scoped_pool() noexcept { current_.set(nullptr); }
273 495 : } guard(this);
274 :
275 : for(;;)
276 : {
277 21929 : continuation* c = nullptr;
278 : {
279 21929 : std::unique_lock<std::mutex> lock(mutex_);
280 21929 : work_cv_.wait(lock, [this]{
281 61885 : return !empty() ||
282 61885 : stop_;
283 : });
284 21929 : if(stop_)
285 990 : return;
286 21434 : c = pop();
287 21929 : }
288 21434 : if(c)
289 21434 : safe_resume(c->h);
290 21434 : }
291 495 : }
292 : };
293 :
294 : //------------------------------------------------------------------------------
295 :
296 1425 : thread_pool::
297 : ~thread_pool()
298 : {
299 1425 : impl_->stop();
300 1425 : impl_->join();
301 1425 : 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 1425 : impl_->drain_abandoned();
305 1425 : destroy();
306 : // Services give up their work in shutdown(), but anything a
307 : // destructor posts anyway is still destroyed.
308 1425 : impl_->drain_abandoned();
309 1425 : delete impl_;
310 1425 : }
311 :
312 1425 : thread_pool::
313 1425 : thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
314 1425 : : impl_(new impl(num_threads, thread_name_prefix))
315 : {
316 1425 : this->set_frame_allocator(std::allocator<void>{});
317 1425 : }
318 :
319 : void
320 1282 : thread_pool::
321 : join() noexcept
322 : {
323 1282 : impl_->join();
324 1282 : }
325 :
326 : void
327 303 : thread_pool::
328 : stop() noexcept
329 : {
330 303 : impl_->stop();
331 303 : }
332 :
333 : //------------------------------------------------------------------------------
334 :
335 : thread_pool::executor_type
336 15344 : thread_pool::
337 : get_executor() const noexcept
338 : {
339 15344 : return executor_type(
340 15344 : const_cast<thread_pool&>(*this));
341 : }
342 :
343 : void
344 4083 : thread_pool::executor_type::
345 : on_work_started() const noexcept
346 : {
347 4083 : pool_->impl_->on_work_started();
348 4083 : }
349 :
350 : void
351 3783 : thread_pool::executor_type::
352 : on_work_finished() const noexcept
353 : {
354 3783 : pool_->impl_->on_work_finished();
355 3783 : }
356 :
357 : void
358 18770 : thread_pool::executor_type::
359 : post(continuation& c) const
360 : {
361 18770 : pool_->impl_->post(c);
362 18768 : }
363 :
364 : std::coroutine_handle<>
365 3206 : thread_pool::executor_type::
366 : dispatch(continuation& c) const
367 : {
368 3206 : if(pool_->impl_->running_in_this_thread())
369 7 : return c.h;
370 3199 : pool_->impl_->post(c);
371 3199 : return std::noop_coroutine();
372 : }
373 :
374 : } // capy
375 : } // boost
|