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