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 20991 : void push(continuation* c) noexcept
73 : {
74 20991 : c->reserved = nullptr;
75 20991 : if(tail_)
76 2400 : tail_->reserved = c;
77 : else
78 18591 : head_ = c;
79 20991 : tail_ = c;
80 20991 : }
81 :
82 23871 : continuation* pop() noexcept
83 : {
84 23871 : if(!head_)
85 2880 : return nullptr;
86 20991 : continuation* c = head_;
87 20991 : head_ = static_cast<continuation*>(head_->reserved);
88 20991 : if(!head_)
89 18591 : tail_ = nullptr;
90 20991 : return c;
91 : }
92 :
93 40012 : bool empty() const noexcept
94 : {
95 40012 : 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 1440 : ~impl() = default;
111 :
112 : bool
113 2721 : running_in_this_thread() const noexcept
114 : {
115 2721 : 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 2880 : drain_abandoned() noexcept
123 : {
124 : for(;;)
125 : {
126 : continuation* c;
127 : {
128 3437 : std::lock_guard<std::mutex> lock(mutex_);
129 3437 : c = pop();
130 3437 : }
131 3437 : if(!c)
132 2880 : return;
133 557 : if(c->h && c->h != std::noop_coroutine())
134 300 : capy::abandon(*c);
135 557 : }
136 : }
137 :
138 1440 : impl(std::size_t num_threads, std::string_view thread_name_prefix)
139 1440 : : num_threads_(num_threads)
140 : {
141 1440 : 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 1440 : auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
147 1440 : thread_name_prefix_[n] = '\0';
148 1440 : }
149 :
150 : void
151 20991 : post(continuation& c)
152 : {
153 20991 : if(!started_.load(std::memory_order_acquire))
154 : {
155 : {
156 603 : std::lock_guard<std::mutex> lock(mutex_);
157 603 : 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 603 : }
165 383 : ensure_started();
166 : }
167 20771 : std::lock_guard<std::mutex> lock(mutex_);
168 20771 : push(&c);
169 : // Under the lock so the pool cannot drain, join, and
170 : // destroy the condition variable mid-signal.
171 20771 : work_cv_.notify_one();
172 20771 : }
173 :
174 : void
175 3598 : on_work_started() noexcept
176 : {
177 3598 : outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
178 3598 : }
179 :
180 : void
181 3298 : on_work_finished() noexcept
182 : {
183 3298 : auto n = outstanding_work_.load(std::memory_order_relaxed);
184 3307 : while(n > 1)
185 : {
186 1402 : if(outstanding_work_.compare_exchange_weak(
187 : n, n - 1, std::memory_order_acq_rel))
188 692 : return;
189 : }
190 2606 : std::lock_guard<std::mutex> lock(mutex_);
191 2606 : outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192 : // Re-read: a concurrent on_work_started() may have raised it.
193 2606 : if(outstanding_work_.load(
194 2606 : std::memory_order_acquire) == 0 && joined_ && !stop_)
195 : {
196 701 : stop_ = true;
197 701 : done_cv_.notify_all();
198 701 : work_cv_.notify_all();
199 : }
200 2606 : }
201 :
202 : void
203 2737 : join() noexcept
204 : {
205 : {
206 2737 : std::unique_lock<std::mutex> lock(mutex_);
207 2737 : if(joined_)
208 1297 : return;
209 1440 : joined_ = true;
210 :
211 1440 : if(outstanding_work_.load(
212 1440 : std::memory_order_acquire) == 0)
213 : {
214 381 : stop_ = true;
215 381 : work_cv_.notify_all();
216 : }
217 : else
218 : {
219 1059 : done_cv_.wait(lock, [this]{
220 2061 : return stop_;
221 : });
222 : }
223 2737 : }
224 :
225 1947 : for(auto& t : threads_)
226 507 : if(t.joinable())
227 507 : t.join();
228 : }
229 :
230 : void
231 1743 : 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 1743 : std::lock_guard<std::mutex> lock(mutex_);
236 1743 : stop_ = true;
237 1743 : work_cv_.notify_all();
238 1743 : done_cv_.notify_all();
239 1743 : }
240 :
241 : private:
242 : void
243 383 : 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 383 : std::lock_guard<std::mutex> lock(mutex_);
249 383 : if(started_.load(std::memory_order_relaxed) || stop_)
250 MIS 0 : return;
251 HIT 383 : threads_.reserve(num_threads_);
252 890 : for(std::size_t i = 0; i < num_threads_; ++i)
253 1014 : threads_.emplace_back([this, i]{ run(i); });
254 383 : started_.store(true, std::memory_order_release);
255 383 : }
256 :
257 : void
258 507 : run(std::size_t index)
259 : {
260 : // Build name; set_current_thread_name truncates to platform limits.
261 : char name[16];
262 507 : std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
263 507 : 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 507 : scoped_pool(impl const* p) noexcept { current_.set(p); }
270 507 : ~scoped_pool() noexcept { current_.set(nullptr); }
271 507 : } guard(this);
272 :
273 : for(;;)
274 : {
275 20941 : continuation* c = nullptr;
276 : {
277 20941 : std::unique_lock<std::mutex> lock(mutex_);
278 20941 : work_cv_.wait(lock, [this]{
279 59487 : return !empty() ||
280 59487 : stop_;
281 : });
282 20941 : if(stop_)
283 1014 : return;
284 20434 : c = pop();
285 20941 : }
286 20434 : if(c)
287 20434 : safe_resume(c->h);
288 20434 : }
289 507 : }
290 : };
291 :
292 : //------------------------------------------------------------------------------
293 :
294 1440 : thread_pool::
295 : ~thread_pool()
296 : {
297 1440 : impl_->stop();
298 1440 : impl_->join();
299 1440 : 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 1440 : impl_->drain_abandoned();
303 1440 : destroy();
304 : // Services give up their work in shutdown(), but anything a
305 : // destructor posts anyway is still destroyed.
306 1440 : impl_->drain_abandoned();
307 1440 : delete impl_;
308 1440 : }
309 :
310 1440 : thread_pool::
311 1440 : thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
312 1440 : : impl_(new impl(num_threads, thread_name_prefix))
313 : {
314 1440 : this->set_frame_allocator(std::allocator<void>{});
315 1440 : }
316 :
317 : void
318 1297 : thread_pool::
319 : join() noexcept
320 : {
321 1297 : impl_->join();
322 1297 : }
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 14857 : thread_pool::
335 : get_executor() const noexcept
336 : {
337 14857 : return executor_type(
338 14857 : const_cast<thread_pool&>(*this));
339 : }
340 :
341 : void
342 3598 : thread_pool::executor_type::
343 : on_work_started() const noexcept
344 : {
345 3598 : pool_->impl_->on_work_started();
346 3598 : }
347 :
348 : void
349 3298 : thread_pool::executor_type::
350 : on_work_finished() const noexcept
351 : {
352 3298 : pool_->impl_->on_work_finished();
353 3298 : }
354 :
355 : void
356 18277 : thread_pool::executor_type::
357 : post(continuation& c) const
358 : {
359 18277 : pool_->impl_->post(c);
360 18277 : }
361 :
362 : std::coroutine_handle<>
363 2721 : thread_pool::executor_type::
364 : dispatch(continuation& c) const
365 : {
366 2721 : if(pool_->impl_->running_in_this_thread())
367 7 : return c.h;
368 2714 : pool_->impl_->post(c);
369 2714 : return std::noop_coroutine();
370 : }
371 :
372 : } // capy
373 : } // boost
|