100.00% Lines (140/140) 100.00% Functions (29/29)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) 2   // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3   // Copyright (c) 2026 Michael Vandeberg 3   // Copyright (c) 2026 Michael Vandeberg
4   // 4   //
5   // Distributed under the Boost Software License, Version 1.0. (See accompanying 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) 6   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
7   // 7   //
8   // Official repository: https://github.com/boostorg/capy 8   // Official repository: https://github.com/boostorg/capy
9   // 9   //
10   10  
11   #include <boost/capy/ex/thread_pool.hpp> 11   #include <boost/capy/ex/thread_pool.hpp>
12   #include <boost/capy/continuation.hpp> 12   #include <boost/capy/continuation.hpp>
13   #include <boost/capy/detail/thread_local_ptr.hpp> 13   #include <boost/capy/detail/thread_local_ptr.hpp>
14   #include <boost/capy/ex/frame_allocator.hpp> 14   #include <boost/capy/ex/frame_allocator.hpp>
15   #include <boost/capy/test/thread_name.hpp> 15   #include <boost/capy/test/thread_name.hpp>
16   #include <algorithm> 16   #include <algorithm>
17   #include <atomic> 17   #include <atomic>
18   #include <condition_variable> 18   #include <condition_variable>
19   #include <cstdio> 19   #include <cstdio>
20   #include <mutex> 20   #include <mutex>
21   #include <thread> 21   #include <thread>
22   #include <vector> 22   #include <vector>
23   23  
24   /* 24   /*
25   Thread pool implementation using a shared work queue. 25   Thread pool implementation using a shared work queue.
26   26  
27   Work items are continuations linked via their intrusive next pointer, 27   Work items are continuations linked via their intrusive next pointer,
28   stored in a single queue protected by a mutex. No per-post heap 28   stored in a single queue protected by a mutex. No per-post heap
29   allocation: the continuation is owned by the caller and linked 29   allocation: the continuation is owned by the caller and linked
30   directly. Worker threads wait on a condition_variable until work 30   directly. Worker threads wait on a condition_variable until work
31   is available or stop is requested. 31   is available or stop is requested.
32   32  
33   Threads are started lazily on first post() via std::call_once to avoid 33   Threads are started lazily on first post() via std::call_once to avoid
34   spawning threads for pools that are constructed but never used. Each 34   spawning threads for pools that are constructed but never used. Each
35   thread is named with a configurable prefix plus index for debugger 35   thread is named with a configurable prefix plus index for debugger
36   visibility. 36   visibility.
37   37  
38   Work tracking: on_work_started/on_work_finished maintain the atomic 38   Work tracking: on_work_started/on_work_finished maintain the atomic
39   outstanding_work_ counter. on_work_started is lock-free; the worker 39   outstanding_work_ counter. on_work_started is lock-free; the worker
40   that drives the count to zero takes mutex_ and re-reads the count 40   that drives the count to zero takes mutex_ and re-reads the count
41   before deciding to stop, so the count and the stop decision stay 41   before deciding to stop, so the count and the stop decision stay
42   consistent even if work is started in between. join() blocks until 42   consistent even if work is started in between. join() blocks until
43   this counter reaches zero, then signals workers to stop and joins 43   this counter reaches zero, then signals workers to stop and joins
44   threads. 44   threads.
45   45  
46   Two shutdown paths: 46   Two shutdown paths:
47   - join(): waits for outstanding work to drain, then stops workers. 47   - join(): waits for outstanding work to drain, then stops workers.
48   - stop(): immediately signals workers to exit; queued work is abandoned. 48   - stop(): immediately signals workers to exit; queued work is abandoned.
49   - Destructor: stop() then join() (abandon + wait for threads). 49   - Destructor: stop() then join() (abandon + wait for threads).
50   */ 50   */
51   51  
52   namespace boost { 52   namespace boost {
53   namespace capy { 53   namespace capy {
54   54  
55   //------------------------------------------------------------------------------ 55   //------------------------------------------------------------------------------
56   56  
57   class thread_pool::impl 57   class thread_pool::impl
58   { 58   {
59   // Identifies the pool owning the current worker thread, or 59   // Identifies the pool owning the current worker thread, or
60   // nullptr if the calling thread is not a pool worker. Checked 60   // nullptr if the calling thread is not a pool worker. Checked
61   // by dispatch() to decide between symmetric transfer (inline 61   // by dispatch() to decide between symmetric transfer (inline
62   // resume) and post. 62   // resume) and post.
63   static inline detail::thread_local_ptr<impl const> current_; 63   static inline detail::thread_local_ptr<impl const> current_;
64   64  
65   // Intrusive queue of continuations: the next link is stored in 65   // Intrusive queue of continuations: the next link is stored in
66   // continuation::reserved (typed continuation* round-tripped through 66   // continuation::reserved (typed continuation* round-tripped through
67   // void*). No per-post allocation: the continuation is owned by the caller. 67   // void*). No per-post allocation: the continuation is owned by the caller.
68   continuation* head_ = nullptr; 68   continuation* head_ = nullptr;
69   continuation* tail_ = nullptr; 69   continuation* tail_ = nullptr;
70   70  
HITCBC 71   20584 void push(continuation* c) noexcept 71   20685 void push(continuation* c) noexcept
72   { 72   {
HITCBC 73   20584 c->reserved = nullptr; 73   20685 c->reserved = nullptr;
HITCBC 74   20584 if(tail_) 74   20685 if(tail_)
HITCBC 75   2335 tail_->reserved = c; 75   2108 tail_->reserved = c;
76   else 76   else
HITCBC 77   18249 head_ = c; 77   18577 head_ = c;
HITCBC 78   20584 tail_ = c; 78   20685 tail_ = c;
HITCBC 79   20584 } 79   20685 }
80   80  
HITCBC 81   20861 continuation* pop() noexcept 81   20964 continuation* pop() noexcept
82   { 82   {
HITCBC 83   20861 if(!head_) 83   20964 if(!head_)
HITCBC 84   277 return nullptr; 84   279 return nullptr;
HITCBC 85   20584 continuation* c = head_; 85   20685 continuation* c = head_;
HITCBC 86   20584 head_ = static_cast<continuation*>(head_->reserved); 86   20685 head_ = static_cast<continuation*>(head_->reserved);
HITCBC 87   20584 if(!head_) 87   20685 if(!head_)
HITCBC 88   18249 tail_ = nullptr; 88   18577 tail_ = nullptr;
HITCBC 89   20584 return c; 89   20685 return c;
90   } 90   }
91   91  
HITCBC 92   40358 bool empty() const noexcept 92   40463 bool empty() const noexcept
93   { 93   {
HITCBC 94   40358 return head_ == nullptr; 94   40463 return head_ == nullptr;
95   } 95   }
96   96  
97   std::mutex mutex_; 97   std::mutex mutex_;
98   std::condition_variable work_cv_; 98   std::condition_variable work_cv_;
99   std::condition_variable done_cv_; 99   std::condition_variable done_cv_;
100   std::vector<std::thread> threads_; 100   std::vector<std::thread> threads_;
101   std::atomic<std::size_t> outstanding_work_{0}; 101   std::atomic<std::size_t> outstanding_work_{0};
102   bool stop_{false}; 102   bool stop_{false};
103   bool joined_{false}; 103   bool joined_{false};
104   std::size_t num_threads_; 104   std::size_t num_threads_;
105   char thread_name_prefix_[13]{}; // 12 chars max + null terminator 105   char thread_name_prefix_[13]{}; // 12 chars max + null terminator
106   std::once_flag start_flag_; 106   std::once_flag start_flag_;
107   107  
108   public: 108   public:
HITCBC 109   277 ~impl() = default; 109   279 ~impl() = default;
110   110  
111   bool 111   bool
HITCBC 112   460 running_in_this_thread() const noexcept 112   462 running_in_this_thread() const noexcept
113   { 113   {
HITCBC 114   460 return current_.get() == this; 114   462 return current_.get() == this;
115   } 115   }
116   116  
117   // Destroy abandoned coroutine frames. Must be called 117   // Destroy abandoned coroutine frames. Must be called
118   // before execution_context::shutdown()/destroy() so 118   // before execution_context::shutdown()/destroy() so
119   // that suspended-frame destructors touching services 119   // that suspended-frame destructors touching services
120   // (e.g. cancelling registrations) run while those 120   // (e.g. cancelling registrations) run while those
121   // services are still valid. 121   // services are still valid.
122   void 122   void
HITCBC 123   277 drain_abandoned() noexcept 123   279 drain_abandoned() noexcept
124   { 124   {
HITCBC 125   483 while(auto* c = pop()) 125   477 while(auto* c = pop())
126   { 126   {
HITCBC 127   206 auto h = c->h; 127   198 auto h = c->h;
HITCBC 128   206 if(h && h != std::noop_coroutine()) 128   198 if(h && h != std::noop_coroutine())
HITCBC 129   155 h.destroy(); 129   147 h.destroy();
HITCBC 130   206 } 130   198 }
HITCBC 131   277 } 131   279 }
132   132  
HITCBC 133   277 impl(std::size_t num_threads, std::string_view thread_name_prefix) 133   279 impl(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 134   277 : num_threads_(num_threads) 134   279 : num_threads_(num_threads)
135   { 135   {
HITCBC 136   277 if(num_threads_ == 0) 136   279 if(num_threads_ == 0)
HITCBC 137   4 num_threads_ = std::max( 137   4 num_threads_ = std::max(
HITCBC 138   2 std::thread::hardware_concurrency(), 1u); 138   2 std::thread::hardware_concurrency(), 1u);
139   139  
140   // Truncate prefix to 12 chars, leaving room for up to 3-digit index. 140   // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
HITCBC 141   277 auto n = thread_name_prefix.copy(thread_name_prefix_, 12); 141   279 auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
HITCBC 142   277 thread_name_prefix_[n] = '\0'; 142   279 thread_name_prefix_[n] = '\0';
HITCBC 143   277 } 143   279 }
144   144  
145   void 145   void
HITCBC 146   20584 post(continuation& c) 146   20685 post(continuation& c)
147   { 147   {
HITCBC 148   20584 ensure_started(); 148   20685 ensure_started();
149   { 149   {
HITCBC 150   20584 std::lock_guard<std::mutex> lock(mutex_); 150   20685 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 151   20584 push(&c); 151   20685 push(&c);
HITCBC 152   20584 } 152   20685 }
HITCBC 153   20584 work_cv_.notify_one(); 153   20685 work_cv_.notify_one();
HITCBC 154   20584 } 154   20685 }
155   155  
156   void 156   void
HITCBC 157   460 on_work_started() noexcept 157   462 on_work_started() noexcept
158   { 158   {
HITCBC 159   460 outstanding_work_.fetch_add(1, std::memory_order_acq_rel); 159   462 outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
HITCBC 160   460 } 160   462 }
161   161  
162   void 162   void
HITCBC 163   460 on_work_finished() noexcept 163   462 on_work_finished() noexcept
164   { 164   {
HITCBC 165   460 if(outstanding_work_.fetch_sub( 165   462 if(outstanding_work_.fetch_sub(
HITCBC 166   460 1, std::memory_order_acq_rel) == 1) 166   462 1, std::memory_order_acq_rel) == 1)
167   { 167   {
168   // fetch_sub's result can be stale: a concurrent 168   // fetch_sub's result can be stale: a concurrent
169   // on_work_started() may raise the count before we take the 169   // on_work_started() may raise the count before we take the
170   // lock, so re-read it here rather than trust the decrement. 170   // lock, so re-read it here rather than trust the decrement.
HITCBC 171   208 std::lock_guard<std::mutex> lock(mutex_); 171   210 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 172   208 if(outstanding_work_.load( 172   210 if(outstanding_work_.load(
HITCBC 173   208 std::memory_order_acquire) == 0 && joined_ && !stop_) 173   210 std::memory_order_acquire) == 0 && joined_ && !stop_)
174   { 174   {
HITCBC 175   79 stop_ = true; 175   69 stop_ = true;
HITCBC 176   79 done_cv_.notify_all(); 176   69 done_cv_.notify_all();
HITCBC 177   79 work_cv_.notify_all(); 177   69 work_cv_.notify_all();
178   } 178   }
HITCBC 179   208 } 179   210 }
HITCBC 180   460 } 180   462 }
181   181  
182   void 182   void
HITCBC 183   424 join() noexcept 183   428 join() noexcept
184   { 184   {
185   { 185   {
HITCBC 186   424 std::unique_lock<std::mutex> lock(mutex_); 186   428 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 187   424 if(joined_) 187   428 if(joined_)
HITCBC 188   147 return; 188   149 return;
HITCBC 189   277 joined_ = true; 189   279 joined_ = true;
190   190  
HITCBC 191   277 if(outstanding_work_.load( 191   279 if(outstanding_work_.load(
HITCBC 192   277 std::memory_order_acquire) == 0) 192   279 std::memory_order_acquire) == 0)
193   { 193   {
HITCBC 194   144 stop_ = true; 194   154 stop_ = true;
HITCBC 195   144 work_cv_.notify_all(); 195   154 work_cv_.notify_all();
196   } 196   }
197   else 197   else
198   { 198   {
HITCBC 199   133 done_cv_.wait(lock, [this]{ 199   125 done_cv_.wait(lock, [this]{
HITCBC 200   213 return stop_; 200   195 return stop_;
201   }); 201   });
202   } 202   }
HITCBC 203   424 } 203   428 }
204   204  
HITCBC 205   588 for(auto& t : threads_) 205   592 for(auto& t : threads_)
HITCBC 206   311 if(t.joinable()) 206   313 if(t.joinable())
HITCBC 207   311 t.join(); 207   313 t.join();
208   } 208   }
209   209  
210   void 210   void
HITCBC 211   279 stop() noexcept 211   281 stop() noexcept
212   { 212   {
213   { 213   {
HITCBC 214   279 std::lock_guard<std::mutex> lock(mutex_); 214   281 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 215   279 stop_ = true; 215   281 stop_ = true;
HITCBC 216   279 } 216   281 }
HITCBC 217   279 work_cv_.notify_all(); 217   281 work_cv_.notify_all();
HITCBC 218   279 done_cv_.notify_all(); 218   281 done_cv_.notify_all();
HITCBC 219   279 } 219   281 }
220   220  
221   private: 221   private:
222   void 222   void
HITCBC 223   20584 ensure_started() 223   20685 ensure_started()
224   { 224   {
HITCBC 225   20584 std::call_once(start_flag_, [this]{ 225   20685 std::call_once(start_flag_, [this]{
HITCBC 226   230 threads_.reserve(num_threads_); 226   232 threads_.reserve(num_threads_);
HITCBC 227   541 for(std::size_t i = 0; i < num_threads_; ++i) 227   545 for(std::size_t i = 0; i < num_threads_; ++i)
HITCBC 228   622 threads_.emplace_back([this, i]{ run(i); }); 228   626 threads_.emplace_back([this, i]{ run(i); });
HITCBC 229   230 }); 229   232 });
HITCBC 230   20584 } 230   20685 }
231   231  
232   void 232   void
HITCBC 233   311 run(std::size_t index) 233   313 run(std::size_t index)
234   { 234   {
235   // Build name; set_current_thread_name truncates to platform limits. 235   // Build name; set_current_thread_name truncates to platform limits.
236   char name[16]; 236   char name[16];
HITCBC 237   311 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); 237   313 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
HITCBC 238   311 set_current_thread_name(name); 238   313 set_current_thread_name(name);
239   239  
240   // Mark this thread as a worker of this pool so dispatch() 240   // Mark this thread as a worker of this pool so dispatch()
241   // can symmetric-transfer when called from within pool work. 241   // can symmetric-transfer when called from within pool work.
242   struct scoped_pool 242   struct scoped_pool
243   { 243   {
HITCBC 244   311 scoped_pool(impl const* p) noexcept { current_.set(p); } 244   313 scoped_pool(impl const* p) noexcept { current_.set(p); }
HITCBC 245   311 ~scoped_pool() noexcept { current_.set(nullptr); } 245   313 ~scoped_pool() noexcept { current_.set(nullptr); }
HITCBC 246   311 } guard(this); 246   313 } guard(this);
247   247  
248   for(;;) 248   for(;;)
249   { 249   {
HITCBC 250   20689 continuation* c = nullptr; 250   20800 continuation* c = nullptr;
251   { 251   {
HITCBC 252   20689 std::unique_lock<std::mutex> lock(mutex_); 252   20800 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 253   20689 work_cv_.wait(lock, [this]{ 253   20800 work_cv_.wait(lock, [this]{
HITCBC 254   60235 return !empty() || 254   60340 return !empty() ||
HITCBC 255   60235 stop_; 255   60340 stop_;
256   }); 256   });
HITCBC 257   20689 if(stop_) 257   20800 if(stop_)
HITCBC 258   622 return; 258   626 return;
HITCBC 259   20378 c = pop(); 259   20487 c = pop();
HITCBC 260   20689 } 260   20800 }
HITCBC 261   20378 if(c) 261   20487 if(c)
HITCBC 262   20378 safe_resume(c->h); 262   20487 safe_resume(c->h);
HITCBC 263   20378 } 263   20487 }
HITCBC 264   311 } 264   313 }
265   }; 265   };
266   266  
267   //------------------------------------------------------------------------------ 267   //------------------------------------------------------------------------------
268   268  
HITCBC 269   277 thread_pool:: 269   279 thread_pool::
270   ~thread_pool() 270   ~thread_pool()
271   { 271   {
HITCBC 272   277 impl_->stop(); 272   279 impl_->stop();
HITCBC 273   277 impl_->join(); 273   279 impl_->join();
HITCBC 274   277 impl_->drain_abandoned(); 274   279 impl_->drain_abandoned();
HITCBC 275   277 shutdown(); 275   279 shutdown();
HITCBC 276   277 destroy(); 276   279 destroy();
HITCBC 277   277 delete impl_; 277   279 delete impl_;
HITCBC 278   277 } 278   279 }
279   279  
HITCBC 280   277 thread_pool:: 280   279 thread_pool::
HITCBC 281   277 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) 281   279 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 282   277 : impl_(new impl(num_threads, thread_name_prefix)) 282   279 : impl_(new impl(num_threads, thread_name_prefix))
283   { 283   {
HITCBC 284   277 this->set_frame_allocator(std::allocator<void>{}); 284   279 this->set_frame_allocator(std::allocator<void>{});
HITCBC 285   277 } 285   279 }
286   286  
287   void 287   void
HITCBC 288   147 thread_pool:: 288   149 thread_pool::
289   join() noexcept 289   join() noexcept
290   { 290   {
HITCBC 291   147 impl_->join(); 291   149 impl_->join();
HITCBC 292   147 } 292   149 }
293   293  
294   void 294   void
HITCBC 295   2 thread_pool:: 295   2 thread_pool::
296   stop() noexcept 296   stop() noexcept
297   { 297   {
HITCBC 298   2 impl_->stop(); 298   2 impl_->stop();
HITCBC 299   2 } 299   2 }
300   300  
301   //------------------------------------------------------------------------------ 301   //------------------------------------------------------------------------------
302   302  
303   thread_pool::executor_type 303   thread_pool::executor_type
HITCBC 304   11683 thread_pool:: 304   11685 thread_pool::
305   get_executor() const noexcept 305   get_executor() const noexcept
306   { 306   {
HITCBC 307   11683 return executor_type( 307   11685 return executor_type(
HITCBC 308   11683 const_cast<thread_pool&>(*this)); 308   11685 const_cast<thread_pool&>(*this));
309   } 309   }
310   310  
311   void 311   void
HITCBC 312   460 thread_pool::executor_type:: 312   462 thread_pool::executor_type::
313   on_work_started() const noexcept 313   on_work_started() const noexcept
314   { 314   {
HITCBC 315   460 pool_->impl_->on_work_started(); 315   462 pool_->impl_->on_work_started();
HITCBC 316   460 } 316   462 }
317   317  
318   void 318   void
HITCBC 319   460 thread_pool::executor_type:: 319   462 thread_pool::executor_type::
320   on_work_finished() const noexcept 320   on_work_finished() const noexcept
321   { 321   {
HITCBC 322   460 pool_->impl_->on_work_finished(); 322   462 pool_->impl_->on_work_finished();
HITCBC 323   460 } 323   462 }
324   324  
325   void 325   void
HITCBC 326   20129 thread_pool::executor_type:: 326   20228 thread_pool::executor_type::
327   post(continuation& c) const 327   post(continuation& c) const
328   { 328   {
HITCBC 329   20129 pool_->impl_->post(c); 329   20228 pool_->impl_->post(c);
HITCBC 330   20129 } 330   20228 }
331   331  
332   std::coroutine_handle<> 332   std::coroutine_handle<>
HITCBC 333   460 thread_pool::executor_type:: 333   462 thread_pool::executor_type::
334   dispatch(continuation& c) const 334   dispatch(continuation& c) const
335   { 335   {
HITCBC 336   460 if(pool_->impl_->running_in_this_thread()) 336   462 if(pool_->impl_->running_in_this_thread())
HITCBC 337   5 return c.h; 337   5 return c.h;
HITCBC 338   455 pool_->impl_->post(c); 338   457 pool_->impl_->post(c);
HITCBC 339   455 return std::noop_coroutine(); 339   457 return std::noop_coroutine();
340   } 340   }
341   341  
342   } // capy 342   } // capy
343   } // boost 343   } // boost