99.34% Lines (150/151) 100.00% Functions (28/28)
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/abandon.hpp> 14   #include <boost/capy/ex/abandon.hpp>
15   #include <boost/capy/ex/frame_allocator.hpp> 15   #include <boost/capy/ex/frame_allocator.hpp>
16   #include <boost/capy/test/thread_name.hpp> 16   #include <boost/capy/test/thread_name.hpp>
17   #include <algorithm> 17   #include <algorithm>
18   #include <atomic> 18   #include <atomic>
19   #include <condition_variable> 19   #include <condition_variable>
20   #include <cstdio> 20   #include <cstdio>
21   #include <mutex> 21   #include <mutex>
22   #include <thread> 22   #include <thread>
23   #include <vector> 23   #include <vector>
24   24  
25   /* 25   /*
26   Thread pool implementation using a shared work queue. 26   Thread pool implementation using a shared work queue.
27   27  
28   Work items are continuations linked via their intrusive next pointer, 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 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 30   allocation: the continuation is owned by the caller and linked
31   directly. Worker threads wait on a condition_variable until work 31   directly. Worker threads wait on a condition_variable until work
32   is available or stop is requested. 32   is available or stop is requested.
33   33  
34   Threads are started lazily on first post(), under mutex_, to avoid 34   Threads are started lazily on first post(), under mutex_, to avoid
35   spawning threads for pools that are constructed but never used. A 35   spawning threads for pools that are constructed but never used. A
36   stopped pool starts none. Each thread is named 36   stopped pool starts none. Each thread is named
37   with a configurable prefix plus index for debugger visibility. 37   with a configurable prefix plus index for debugger visibility.
38   38  
39   Work tracking: on_work_started/on_work_finished maintain the atomic 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 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 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, 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. 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 44   join() blocks until this counter reaches zero, then signals workers
45   to stop and joins threads. 45   to stop and joins threads.
46   46  
47   Two shutdown paths: 47   Two shutdown paths:
48   - join(): waits for outstanding work to drain, then stops workers. 48   - join(): waits for outstanding work to drain, then stops workers.
49   - stop(): immediately signals workers to exit; queued work stays queued. 49   - stop(): immediately signals workers to exit; queued work stays queued.
50   - Destructor: stop() then join(), then queued work is abandoned. 50   - Destructor: stop() then join(), then queued work is abandoned.
51   */ 51   */
52   52  
53   namespace boost { 53   namespace boost {
54   namespace capy { 54   namespace capy {
55   55  
56   //------------------------------------------------------------------------------ 56   //------------------------------------------------------------------------------
57   57  
58   class thread_pool::impl 58   class thread_pool::impl
59   { 59   {
60   // Identifies the pool owning the current worker thread, or 60   // Identifies the pool owning the current worker thread, or
61   // nullptr if the calling thread is not a pool worker. Checked 61   // nullptr if the calling thread is not a pool worker. Checked
62   // by dispatch() to decide between symmetric transfer (inline 62   // by dispatch() to decide between symmetric transfer (inline
63   // resume) and post. 63   // resume) and post.
64   static inline detail::thread_local_ptr<impl const> current_; 64   static inline detail::thread_local_ptr<impl const> current_;
65   65  
66   // Intrusive queue of continuations: the next link is stored in 66   // Intrusive queue of continuations: the next link is stored in
67   // continuation::reserved (typed continuation* round-tripped through 67   // continuation::reserved (typed continuation* round-tripped through
68   // void*). No per-post allocation: the continuation is owned by the caller. 68   // void*). No per-post allocation: the continuation is owned by the caller.
69   continuation* head_ = nullptr; 69   continuation* head_ = nullptr;
70   continuation* tail_ = nullptr; 70   continuation* tail_ = nullptr;
71   71  
HITCBC 72   24470 void push(continuation* c) noexcept 72   20991 void push(continuation* c) noexcept
73   { 73   {
HITCBC 74   24470 c->reserved = nullptr; 74   20991 c->reserved = nullptr;
HITCBC 75   24470 if(tail_) 75   20991 if(tail_)
HITCBC 76   4330 tail_->reserved = c; 76   2400 tail_->reserved = c;
77   else 77   else
HITCBC 78   20140 head_ = c; 78   18591 head_ = c;
HITCBC 79   24470 tail_ = c; 79   20991 tail_ = c;
HITCBC 80   24470 } 80   20991 }
81   81  
HITCBC 82   27315 continuation* pop() noexcept 82   23871 continuation* pop() noexcept
83   { 83   {
HITCBC 84   27315 if(!head_) 84   23871 if(!head_)
HITCBC 85   2845 return nullptr; 85   2880 return nullptr;
HITCBC 86   24470 continuation* c = head_; 86   20991 continuation* c = head_;
HITCBC 87   24470 head_ = static_cast<continuation*>(head_->reserved); 87   20991 head_ = static_cast<continuation*>(head_->reserved);
HITCBC 88   24470 if(!head_) 88   20991 if(!head_)
HITCBC 89   20140 tail_ = nullptr; 89   18591 tail_ = nullptr;
HITCBC 90   24470 return c; 90   20991 return c;
91   } 91   }
92   92  
HITCBC 93   42608 bool empty() const noexcept 93   40012 bool empty() const noexcept
94   { 94   {
HITCBC 95   42608 return head_ == nullptr; 95   40012 return head_ == nullptr;
96   } 96   }
97   97  
98   std::mutex mutex_; 98   std::mutex mutex_;
99   std::condition_variable work_cv_; 99   std::condition_variable work_cv_;
100   std::condition_variable done_cv_; 100   std::condition_variable done_cv_;
101   std::vector<std::thread> threads_; 101   std::vector<std::thread> threads_;
102   std::atomic<std::size_t> outstanding_work_{0}; 102   std::atomic<std::size_t> outstanding_work_{0};
103   bool stop_{false}; 103   bool stop_{false};
104   bool joined_{false}; 104   bool joined_{false};
105   std::size_t num_threads_; 105   std::size_t num_threads_;
106   char thread_name_prefix_[13]{}; // 12 chars max + null terminator 106   char thread_name_prefix_[13]{}; // 12 chars max + null terminator
107   std::atomic<bool> started_{false}; 107   std::atomic<bool> started_{false};
108   108  
109   public: 109   public:
HITCBC 110   1421 ~impl() = default; 110   1440 ~impl() = default;
111   111  
112   bool 112   bool
HITCBC 113   3202 running_in_this_thread() const noexcept 113   2721 running_in_this_thread() const noexcept
114   { 114   {
HITCBC 115   3202 return current_.get() == this; 115   2721 return current_.get() == this;
116   } 116   }
117   117  
118 - // Abandon queued coroutines. Called after the services shut down, 118 + // Abandon queued coroutines. Each is taken under the lock, as other
119 - // since their shutdown can post more, and again after they are 119 + // threads may still post, and abandoned outside it, as abandoning
120 - // destroyed, for anything their destructors post. Each is taken 120 + // can post.
121 - // under the lock, as other threads may still post, and abandoned 121 + void
122 - // outside it, as abandoning can post. Returns whether there was  
123 - // anything to abandon.  
124 - bool  
HITCBC 125   2845 drain_abandoned() noexcept 122   2880 drain_abandoned() noexcept
126 - bool any = false;  
ECB 127   2845 { 123   {
128   for(;;) 124   for(;;)
129   { 125   {
130   continuation* c; 126   continuation* c;
131   { 127   {
HITCBC 132   5344 std::lock_guard<std::mutex> lock(mutex_); 128   3437 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 133   5344 c = pop(); 129   3437 c = pop();
HITCBC 134   5344 } 130   3437 }
HITCBC 135   5344 if(!c) 131   3437 if(!c)
HITCBC 136 - 2845 return any; 132 + 2880 return;
DCB 137 - 2499 any = true;  
HITCBC 138   2499 if(c->h && c->h != std::noop_coroutine()) 133   557 if(c->h && c->h != std::noop_coroutine())
HITCBC 139   2248 capy::abandon(*c); 134   300 capy::abandon(*c);
HITCBC 140   2499 } 135   557 }
141   } 136   }
142   137  
HITCBC 143   1421 impl(std::size_t num_threads, std::string_view thread_name_prefix) 138   1440 impl(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 144   1421 : num_threads_(num_threads) 139   1440 : num_threads_(num_threads)
145   { 140   {
HITCBC 146   1421 if(num_threads_ == 0) 141   1440 if(num_threads_ == 0)
HITCBC 147   8 num_threads_ = std::max( 142   8 num_threads_ = std::max(
HITCBC 148   4 std::thread::hardware_concurrency(), 1u); 143   4 std::thread::hardware_concurrency(), 1u);
149   144  
150   // Truncate prefix to 12 chars, leaving room for up to 3-digit index. 145   // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
HITCBC 151   1421 auto n = thread_name_prefix.copy(thread_name_prefix_, 12); 146   1440 auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
HITCBC 152   1421 thread_name_prefix_[n] = '\0'; 147   1440 thread_name_prefix_[n] = '\0';
HITCBC 153   1421 } 148   1440 }
154   149  
155   void 150   void
HITCBC 156   24470 post(continuation& c) 151   20991 post(continuation& c)
157   { 152   {
HITCBC 158   24470 if(!started_.load(std::memory_order_acquire)) 153   20991 if(!started_.load(std::memory_order_acquire))
159   { 154   {
160   { 155   {
HITCBC 161   2568 std::lock_guard<std::mutex> lock(mutex_); 156   603 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 162   2568 if(stop_) 157   603 if(stop_)
163   { 158   {
164   // Stopped before any thread started: nothing will 159   // Stopped before any thread started: nothing will
165   // run it, and the destructor's drain abandons it. 160   // run it, and the destructor's drain abandons it.
HITCBC 166   2204 push(&c); 161   220 push(&c);
HITCBC 167   2204 return; 162   220 return;
168   } 163   }
HITCBC 169   2568 } 164   603 }
HITCBC 170   364 ensure_started(); 165   383 ensure_started();
171   } 166   }
HITCBC 172   22266 std::lock_guard<std::mutex> lock(mutex_); 167   20771 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 173   22266 push(&c); 168   20771 push(&c);
174   // Under the lock so the pool cannot drain, join, and 169   // Under the lock so the pool cannot drain, join, and
175   // destroy the condition variable mid-signal. 170   // destroy the condition variable mid-signal.
HITCBC 176   22266 work_cv_.notify_one(); 171   20771 work_cv_.notify_one();
HITCBC 177   22266 } 172   20771 }
178   173  
179   void 174   void
HITCBC 180   4079 on_work_started() noexcept 175   3598 on_work_started() noexcept
181   { 176   {
HITCBC 182   4079 outstanding_work_.fetch_add(1, std::memory_order_acq_rel); 177   3598 outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
HITCBC 183   4079 } 178   3598 }
184   179  
185   void 180   void
HITCBC 186   3779 on_work_finished() noexcept 181   3298 on_work_finished() noexcept
187   { 182   {
HITCBC 188   3779 auto n = outstanding_work_.load(std::memory_order_relaxed); 183   3298 auto n = outstanding_work_.load(std::memory_order_relaxed);
HITCBC 189   3790 while(n > 1) 184   3307 while(n > 1)
190   { 185   {
HITCBC 191   2640 if(outstanding_work_.compare_exchange_weak( 186   1402 if(outstanding_work_.compare_exchange_weak(
192   n, n - 1, std::memory_order_acq_rel)) 187   n, n - 1, std::memory_order_acq_rel))
HITCBC 193   1309 return; 188   692 return;
194   } 189   }
HITCBC 195   2470 std::lock_guard<std::mutex> lock(mutex_); 190   2606 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 196   2470 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel); 191   2606 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
197   // Re-read: a concurrent on_work_started() may have raised it. 192   // Re-read: a concurrent on_work_started() may have raised it.
HITCBC 198   2470 if(outstanding_work_.load( 193   2606 if(outstanding_work_.load(
HITCBC 199   2470 std::memory_order_acquire) == 0 && joined_ && !stop_) 194   2606 std::memory_order_acquire) == 0 && joined_ && !stop_)
200   { 195   {
HITCBC 201   711 stop_ = true; 196   701 stop_ = true;
HITCBC 202   711 done_cv_.notify_all(); 197   701 done_cv_.notify_all();
HITCBC 203   711 work_cv_.notify_all(); 198   701 work_cv_.notify_all();
204   } 199   }
HITCBC 205   2470 } 200   2606 }
206   201  
207   void 202   void
HITCBC 208   2699 join() noexcept 203   2737 join() noexcept
209   { 204   {
210   { 205   {
HITCBC 211   2699 std::unique_lock<std::mutex> lock(mutex_); 206   2737 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 212   2699 if(joined_) 207   2737 if(joined_)
HITCBC 213   1278 return; 208   1297 return;
HITCBC 214   1421 joined_ = true; 209   1440 joined_ = true;
215   210  
HITCBC 216   1421 if(outstanding_work_.load( 211   1440 if(outstanding_work_.load(
HITCBC 217   1421 std::memory_order_acquire) == 0) 212   1440 std::memory_order_acquire) == 0)
218   { 213   {
HITCBC 219   354 stop_ = true; 214   381 stop_ = true;
HITCBC 220   354 work_cv_.notify_all(); 215   381 work_cv_.notify_all();
221   } 216   }
222   else 217   else
223   { 218   {
HITCBC 224   1067 done_cv_.wait(lock, [this]{ 219   1059 done_cv_.wait(lock, [this]{
HITCBC 225   2079 return stop_; 220   2061 return stop_;
226   }); 221   });
227   } 222   }
HITCBC 228   2699 } 223   2737 }
229   224  
HITCBC 230   1910 for(auto& t : threads_) 225   1947 for(auto& t : threads_)
HITCBC 231   489 if(t.joinable()) 226   507 if(t.joinable())
HITCBC 232   489 t.join(); 227   507 t.join();
233   } 228   }
234   229  
235   void 230   void
HITCBC 236   1724 stop() noexcept 231   1743 stop() noexcept
237   { 232   {
238   // Under the lock: the join() this releases may return and let 233   // Under the lock: the join() this releases may return and let
239   // the pool be destroyed as soon as the lock is free. 234   // the pool be destroyed as soon as the lock is free.
HITCBC 240   1724 std::lock_guard<std::mutex> lock(mutex_); 235   1743 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 241   1724 stop_ = true; 236   1743 stop_ = true;
HITCBC 242   1724 work_cv_.notify_all(); 237   1743 work_cv_.notify_all();
HITCBC 243   1724 done_cv_.notify_all(); 238   1743 done_cv_.notify_all();
HITCBC 244   1724 } 239   1743 }
245   240  
246   private: 241   private:
247   void 242   void
HITCBC 248   364 ensure_started() 243   383 ensure_started()
249   { 244   {
250   // Under the lock, so join() either sees every thread started 245   // Under the lock, so join() either sees every thread started
251   // here or makes this start none. A joined pool still starts them: 246   // here or makes this start none. A joined pool still starts them:
252   // join() may be waiting on work that needs them. 247   // join() may be waiting on work that needs them.
HITCBC 253   364 std::lock_guard<std::mutex> lock(mutex_); 248   383 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 254   364 if(started_.load(std::memory_order_relaxed) || stop_) 249   383 if(started_.load(std::memory_order_relaxed) || stop_)
MISUBC 255   ✗ return; 250   ✗ return;
HITCBC 256   364 threads_.reserve(num_threads_); 251   383 threads_.reserve(num_threads_);
HITCBC 257   853 for(std::size_t i = 0; i < num_threads_; ++i) 252   890 for(std::size_t i = 0; i < num_threads_; ++i)
HITCBC 258   978 threads_.emplace_back([this, i]{ run(i); }); 253   1014 threads_.emplace_back([this, i]{ run(i); });
HITCBC 259   364 started_.store(true, std::memory_order_release); 254   383 started_.store(true, std::memory_order_release);
HITCBC 260   364 } 255   383 }
261   256  
262   void 257   void
HITCBC 263   489 run(std::size_t index) 258   507 run(std::size_t index)
264   { 259   {
265   // Build name; set_current_thread_name truncates to platform limits. 260   // Build name; set_current_thread_name truncates to platform limits.
266   char name[16]; 261   char name[16];
HITCBC 267   489 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); 262   507 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
HITCBC 268   489 set_current_thread_name(name); 263   507 set_current_thread_name(name);
269   264  
270   // Mark this thread as a worker of this pool so dispatch() 265   // Mark this thread as a worker of this pool so dispatch()
271   // can symmetric-transfer when called from within pool work. 266   // can symmetric-transfer when called from within pool work.
272   struct scoped_pool 267   struct scoped_pool
273   { 268   {
HITCBC 274   489 scoped_pool(impl const* p) noexcept { current_.set(p); } 269   507 scoped_pool(impl const* p) noexcept { current_.set(p); }
HITCBC 275   489 ~scoped_pool() noexcept { current_.set(nullptr); } 270   507 ~scoped_pool() noexcept { current_.set(nullptr); }
HITCBC 276   489 } guard(this); 271   507 } guard(this);
277   272  
278   for(;;) 273   for(;;)
279   { 274   {
HITCBC 280   22460 continuation* c = nullptr; 275   20941 continuation* c = nullptr;
281   { 276   {
HITCBC 282   22460 std::unique_lock<std::mutex> lock(mutex_); 277   20941 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 283   22460 work_cv_.wait(lock, [this]{ 278   20941 work_cv_.wait(lock, [this]{
HITCBC 284   63152 return !empty() || 279   59487 return !empty() ||
HITCBC 285   63152 stop_; 280   59487 stop_;
286   }); 281   });
HITCBC 287   22460 if(stop_) 282   20941 if(stop_)
HITCBC 288   978 return; 283   1014 return;
HITCBC 289   21971 c = pop(); 284   20434 c = pop();
HITCBC 290   22460 } 285   20941 }
HITCBC 291   21971 if(c) 286   20434 if(c)
HITCBC 292   21971 safe_resume(c->h); 287   20434 safe_resume(c->h);
HITCBC 293   21971 } 288   20434 }
HITCBC 294   489 } 289   507 }
295   }; 290   };
296   291  
297   //------------------------------------------------------------------------------ 292   //------------------------------------------------------------------------------
298   293  
HITCBC 299   1421 thread_pool:: 294   1440 thread_pool::
300   ~thread_pool() 295   ~thread_pool()
301   { 296   {
HITCBC 302   1421 impl_->stop(); 297   1440 impl_->stop();
HITCBC 303   1421 impl_->join(); 298   1440 impl_->join();
HITCBC 304   1421 shutdown(); 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.
HITGNC   302 + 1440 impl_->drain_abandoned();
HITGNC   303 + 1440 destroy();
  304 + // Services give up their work in shutdown(), but anything a
  305 + // destructor posts anyway is still destroyed.
DCB 305 - 1421 // A frame the drain unwinds can create a service, whose destructor  
306 - // can post again: repeat until nothing is left of either.  
307 - do  
308 - {  
309 - destroy();  
DCB 310 - 1424 }  
311 - while(impl_->drain_abandoned());  
HITCBC 312   1424 impl_->drain_abandoned(); 306   1440 impl_->drain_abandoned();
HITCBC 313   1421 delete impl_; 307   1440 delete impl_;
HITCBC 314   1421 } 308   1440 }
315   309  
HITCBC 316   1421 thread_pool:: 310   1440 thread_pool::
HITCBC 317   1421 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) 311   1440 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 318   1421 : impl_(new impl(num_threads, thread_name_prefix)) 312   1440 : impl_(new impl(num_threads, thread_name_prefix))
319   { 313   {
HITCBC 320   1421 this->set_frame_allocator(std::allocator<void>{}); 314   1440 this->set_frame_allocator(std::allocator<void>{});
HITCBC 321   1421 } 315   1440 }
322   316  
323   void 317   void
HITCBC 324   1278 thread_pool:: 318   1297 thread_pool::
325   join() noexcept 319   join() noexcept
326   { 320   {
HITCBC 327   1278 impl_->join(); 321   1297 impl_->join();
HITCBC 328   1278 } 322   1297 }
329   323  
330   void 324   void
HITCBC 331   303 thread_pool:: 325   303 thread_pool::
332   stop() noexcept 326   stop() noexcept
333   { 327   {
HITCBC 334   303 impl_->stop(); 328   303 impl_->stop();
HITCBC 335   303 } 329   303 }
336   330  
337   //------------------------------------------------------------------------------ 331   //------------------------------------------------------------------------------
338   332  
339   thread_pool::executor_type 333   thread_pool::executor_type
HITCBC 340   15338 thread_pool:: 334   14857 thread_pool::
341   get_executor() const noexcept 335   get_executor() const noexcept
342   { 336   {
HITCBC 343   15338 return executor_type( 337   14857 return executor_type(
HITCBC 344   15338 const_cast<thread_pool&>(*this)); 338   14857 const_cast<thread_pool&>(*this));
345   } 339   }
346   340  
347   void 341   void
HITCBC 348   4079 thread_pool::executor_type:: 342   3598 thread_pool::executor_type::
349   on_work_started() const noexcept 343   on_work_started() const noexcept
350   { 344   {
HITCBC 351   4079 pool_->impl_->on_work_started(); 345   3598 pool_->impl_->on_work_started();
HITCBC 352   4079 } 346   3598 }
353   347  
354   void 348   void
HITCBC 355   3779 thread_pool::executor_type:: 349   3298 thread_pool::executor_type::
356   on_work_finished() const noexcept 350   on_work_finished() const noexcept
357   { 351   {
HITCBC 358   3779 pool_->impl_->on_work_finished(); 352   3298 pool_->impl_->on_work_finished();
HITCBC 359   3779 } 353   3298 }
360   354  
361   void 355   void
HITCBC 362   21275 thread_pool::executor_type:: 356   18277 thread_pool::executor_type::
363   post(continuation& c) const 357   post(continuation& c) const
364   { 358   {
HITCBC 365   21275 pool_->impl_->post(c); 359   18277 pool_->impl_->post(c);
HITCBC 366   21275 } 360   18277 }
367   361  
368   std::coroutine_handle<> 362   std::coroutine_handle<>
HITCBC 369   3202 thread_pool::executor_type:: 363   2721 thread_pool::executor_type::
370   dispatch(continuation& c) const 364   dispatch(continuation& c) const
371   { 365   {
HITCBC 372   3202 if(pool_->impl_->running_in_this_thread()) 366   2721 if(pool_->impl_->running_in_this_thread())
HITCBC 373   7 return c.h; 367   7 return c.h;
HITCBC 374   3195 pool_->impl_->post(c); 368   2714 pool_->impl_->post(c);
HITCBC 375   3195 return std::noop_coroutine(); 369   2714 return std::noop_coroutine();
376   } 370   }
377   371  
378   } // capy 372   } // capy
379   } // boost 373   } // boost