100.00% Lines (151/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   22295 void push(continuation* c) noexcept 72   23945 void push(continuation* c) noexcept
73   { 73   {
HITCBC 74   22295 c->reserved = nullptr; 74   23945 c->reserved = nullptr;
HITCBC 75   22295 if(tail_) 75   23945 if(tail_)
HITCBC 76   1603 tail_->reserved = c; 76   3075 tail_->reserved = c;
77   else 77   else
HITCBC 78   20692 head_ = c; 78   20870 head_ = c;
HITCBC 79   22295 tail_ = c; 79   23945 tail_ = c;
HITCBC 80   22295 } 80   23945 }
81   81  
HITCBC 82   25143 continuation* pop() noexcept 82   26747 continuation* pop() noexcept
83   { 83   {
HITCBC 84   25143 if(!head_) 84   26747 if(!head_)
HITCBC 85   2848 return nullptr; 85   2802 return nullptr;
HITCBC 86   22295 continuation* c = head_; 86   23945 continuation* c = head_;
HITCBC 87   22295 head_ = static_cast<continuation*>(head_->reserved); 87   23945 head_ = static_cast<continuation*>(head_->reserved);
HITCBC 88   22295 if(!head_) 88   23945 if(!head_)
HITCBC 89   20692 tail_ = nullptr; 89   20870 tail_ = nullptr;
HITCBC 90   22295 return c; 90   23945 return c;
91   } 91   }
92   92  
HITCBC 93   42878 bool empty() const noexcept 93   44837 bool empty() const noexcept
94   { 94   {
HITCBC 95   42878 return head_ == nullptr; 95   44837 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   1424 ~impl() = default; 110   1401 ~impl() = default;
111   111  
112   bool 112   bool
HITCBC 113   3207 running_in_this_thread() const noexcept 113   3184 running_in_this_thread() const noexcept
114   { 114   {
HITCBC 115   3207 return current_.get() == this; 115   3184 return current_.get() == this;
116   } 116   }
117   117  
118   // Abandon queued coroutines. Each is taken under the lock, as other 118   // Abandon queued coroutines. Each is taken under the lock, as other
119   // threads may still post, and abandoned outside it, as abandoning 119   // threads may still post, and abandoned outside it, as abandoning
120   // can post. 120   // can post.
121   void 121   void
HITCBC 122   2848 drain_abandoned() noexcept 122   2802 drain_abandoned() noexcept
123   { 123   {
124   for(;;) 124   for(;;)
125   { 125   {
126   continuation* c; 126   continuation* c;
127   { 127   {
HITCBC 128   3401 std::lock_guard<std::mutex> lock(mutex_); 128   3409 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 129   3401 c = pop(); 129   3409 c = pop();
HITCBC 130   3401 } 130   3409 }
HITCBC 131   3401 if(!c) 131   3409 if(!c)
HITCBC 132   2848 return; 132   2802 return;
HITCBC 133   553 if(c->h && c->h != std::noop_coroutine()) 133   607 if(c->h && c->h != std::noop_coroutine())
HITCBC 134   302 capy::abandon(*c); 134   356 capy::abandon(*c);
HITCBC 135   553 } 135   607 }
136   } 136   }
137   137  
HITCBC 138   1424 impl(std::size_t num_threads, std::string_view thread_name_prefix) 138   1401 impl(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 139   1424 : num_threads_(num_threads) 139   1401 : num_threads_(num_threads)
140   { 140   {
HITCBC 141   1424 if(num_threads_ == 0) 141   1401 if(num_threads_ == 0)
HITCBC 142   8 num_threads_ = std::max( 142   8 num_threads_ = std::max(
HITCBC 143   4 std::thread::hardware_concurrency(), 1u); 143   4 std::thread::hardware_concurrency(), 1u);
144   144  
145   // 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 146   1424 auto n = thread_name_prefix.copy(thread_name_prefix_, 12); 146   1401 auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
HITCBC 147   1424 thread_name_prefix_[n] = '\0'; 147   1401 thread_name_prefix_[n] = '\0';
HITCBC 148   1424 } 148   1401 }
149   149  
150   void 150   void
HITCBC 151   22295 post(continuation& c) 151   23945 post(continuation& c)
152   { 152   {
HITCBC 153   22295 if(!started_.load(std::memory_order_acquire)) 153   23945 if(!started_.load(std::memory_order_acquire))
154   { 154   {
155   { 155   {
HITCBC 156   592 std::lock_guard<std::mutex> lock(mutex_); 156   567 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 157   592 if(stop_) 157   567 if(stop_)
158   { 158   {
159   // Stopped before any thread started: nothing will 159   // Stopped before any thread started: nothing will
160   // run it, and the destructor's drain abandons it. 160   // run it, and the destructor's drain abandons it.
HITCBC 161   220 push(&c); 161   220 push(&c);
HITCBC 162   220 return; 162   220 return;
163   } 163   }
HITCBC 164   592 } 164   567 }
HITCBC 165   372 ensure_started(); 165   347 ensure_started();
166   } 166   }
HITCBC 167   22075 std::lock_guard<std::mutex> lock(mutex_); 167   23725 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 168   22075 push(&c); 168   23725 push(&c);
169   // Under the lock so the pool cannot drain, join, and 169   // Under the lock so the pool cannot drain, join, and
170   // destroy the condition variable mid-signal. 170   // destroy the condition variable mid-signal.
HITCBC 171   22075 work_cv_.notify_one(); 171   23725 work_cv_.notify_one();
HITCBC 172   22075 } 172   23725 }
173   173  
174   void 174   void
HITCBC 175   4084 on_work_started() noexcept 175   4061 on_work_started() noexcept
176   { 176   {
HITCBC 177   4084 outstanding_work_.fetch_add(1, std::memory_order_acq_rel); 177   4061 outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
HITCBC 178   4084 } 178   4061 }
179   179  
180   void 180   void
HITCBC 181   3784 on_work_finished() noexcept 181   3761 on_work_finished() noexcept
182   { 182   {
HITCBC 183   3784 auto n = outstanding_work_.load(std::memory_order_relaxed); 183   3761 auto n = outstanding_work_.load(std::memory_order_relaxed);
HITCBC 184   3789 while(n > 1) 184   3766 while(n > 1)
185   { 185   {
HITCBC 186   2504 if(outstanding_work_.compare_exchange_weak( 186   902 if(outstanding_work_.compare_exchange_weak(
187   n, n - 1, std::memory_order_acq_rel)) 187   n, n - 1, std::memory_order_acq_rel))
HITCBC 188   1247 return; 188   446 return;
189   } 189   }
HITCBC 190   2537 std::lock_guard<std::mutex> lock(mutex_); 190   3315 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 191   2537 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel); 191   3315 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
192   // Re-read: a concurrent on_work_started() may have raised it. 192   // Re-read: a concurrent on_work_started() may have raised it.
HITCBC 193   2537 if(outstanding_work_.load( 193   3315 if(outstanding_work_.load(
HITCBC 194   2537 std::memory_order_acquire) == 0 && joined_ && !stop_) 194   3315 std::memory_order_acquire) == 0 && joined_ && !stop_)
195   { 195   {
HITCBC 196   718 stop_ = true; 196   646 stop_ = true;
HITCBC 197   718 done_cv_.notify_all(); 197   646 done_cv_.notify_all();
HITCBC 198   718 work_cv_.notify_all(); 198   646 work_cv_.notify_all();
199   } 199   }
HITCBC 200   2537 } 200   3315 }
201   201  
202   void 202   void
HITCBC 203   2705 join() noexcept 203   2659 join() noexcept
204   { 204   {
205   { 205   {
HITCBC 206   2705 std::unique_lock<std::mutex> lock(mutex_); 206   2659 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 207   2705 if(joined_) 207   2659 if(joined_)
HITCBC 208   1281 return; 208   1258 return;
HITCBC 209   1424 joined_ = true; 209   1401 joined_ = true;
210   210  
HITCBC 211   1424 if(outstanding_work_.load( 211   1401 if(outstanding_work_.load(
HITCBC 212   1424 std::memory_order_acquire) == 0) 212   1401 std::memory_order_acquire) == 0)
213   { 213   {
HITCBC 214   350 stop_ = true; 214   396 stop_ = true;
HITCBC 215   350 work_cv_.notify_all(); 215   396 work_cv_.notify_all();
216   } 216   }
217   else 217   else
218   { 218   {
HITCBC 219   1074 done_cv_.wait(lock, [this]{ 219   1005 done_cv_.wait(lock, [this]{
HITCBC 220   2093 return stop_; 220   1952 return stop_;
221   }); 221   });
222   } 222   }
HITCBC 223   2705 } 223   2659 }
224   224  
HITCBC 225   1916 for(auto& t : threads_) 225   1870 for(auto& t : threads_)
HITCBC 226   492 if(t.joinable()) 226   469 if(t.joinable())
HITCBC 227   492 t.join(); 227   469 t.join();
228   } 228   }
229   229  
230   void 230   void
HITCBC 231   1727 stop() noexcept 231   1704 stop() noexcept
232   { 232   {
233   // Under the lock: the join() this releases may return and let 233   // Under the lock: the join() this releases may return and let
234   // the pool be destroyed as soon as the lock is free. 234   // the pool be destroyed as soon as the lock is free.
HITCBC 235   1727 std::lock_guard<std::mutex> lock(mutex_); 235   1704 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 236   1727 stop_ = true; 236   1704 stop_ = true;
HITCBC 237   1727 work_cv_.notify_all(); 237   1704 work_cv_.notify_all();
HITCBC 238   1727 done_cv_.notify_all(); 238   1704 done_cv_.notify_all();
HITCBC 239   1727 } 239   1704 }
240   240  
241   private: 241   private:
242   void 242   void
HITCBC 243   372 ensure_started() 243   347 ensure_started()
244   { 244   {
245   // Under the lock, so join() either sees every thread started 245   // Under the lock, so join() either sees every thread started
246   // 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:
247   // join() may be waiting on work that needs them. 247   // join() may be waiting on work that needs them.
HITCBC 248   372 std::lock_guard<std::mutex> lock(mutex_); 248   347 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 249   372 if(started_.load(std::memory_order_relaxed) || stop_) 249   347 if(started_.load(std::memory_order_relaxed) || stop_)
HITCBC 250   5 return; 250   3 return;
HITCBC 251   367 threads_.reserve(num_threads_); 251   344 threads_.reserve(num_threads_);
HITCBC 252   859 for(std::size_t i = 0; i < num_threads_; ++i) 252   813 for(std::size_t i = 0; i < num_threads_; ++i)
HITCBC 253   984 threads_.emplace_back([this, i]{ run(i); }); 253   938 threads_.emplace_back([this, i]{ run(i); });
HITCBC 254   367 started_.store(true, std::memory_order_release); 254   344 started_.store(true, std::memory_order_release);
HITCBC 255   372 } 255   347 }
256   256  
257   void 257   void
HITCBC 258   492 run(std::size_t index) 258   469 run(std::size_t index)
259   { 259   {
260   // Build name; set_current_thread_name truncates to platform limits. 260   // Build name; set_current_thread_name truncates to platform limits.
261   char name[16]; 261   char name[16];
HITCBC 262   492 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); 262   469 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
HITCBC 263   492 set_current_thread_name(name); 263   469 set_current_thread_name(name);
264   264  
265   // Mark this thread as a worker of this pool so dispatch() 265   // Mark this thread as a worker of this pool so dispatch()
266   // can symmetric-transfer when called from within pool work. 266   // can symmetric-transfer when called from within pool work.
267   struct scoped_pool 267   struct scoped_pool
268   { 268   {
HITCBC 269   492 scoped_pool(impl const* p) noexcept { current_.set(p); } 269   469 scoped_pool(impl const* p) noexcept { current_.set(p); }
HITCBC 270   492 ~scoped_pool() noexcept { current_.set(nullptr); } 270   469 ~scoped_pool() noexcept { current_.set(nullptr); }
HITCBC 271   492 } guard(this); 271   469 } guard(this);
272   272  
273   for(;;) 273   for(;;)
274   { 274   {
HITCBC 275   22234 continuation* c = nullptr; 275   23807 continuation* c = nullptr;
276   { 276   {
HITCBC 277   22234 std::unique_lock<std::mutex> lock(mutex_); 277   23807 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 278   22234 work_cv_.wait(lock, [this]{ 278   23807 work_cv_.wait(lock, [this]{
HITCBC 279   63915 return !empty() || 279   66235 return !empty() ||
HITCBC 280   63915 stop_; 280   66235 stop_;
281   }); 281   });
HITCBC 282   22234 if(stop_) 282   23807 if(stop_)
HITCBC 283   984 return; 283   938 return;
HITCBC 284   21742 c = pop(); 284   23338 c = pop();
HITCBC 285   22234 } 285   23807 }
HITCBC 286   21742 if(c) 286   23338 if(c)
HITCBC 287   21742 safe_resume(c->h); 287   23338 safe_resume(c->h);
HITCBC 288   21742 } 288   23338 }
HITCBC 289   492 } 289   469 }
290   }; 290   };
291   291  
292   //------------------------------------------------------------------------------ 292   //------------------------------------------------------------------------------
293   293  
HITCBC 294   1424 thread_pool:: 294   1401 thread_pool::
295   ~thread_pool() 295   ~thread_pool()
296   { 296   {
HITCBC 297   1424 impl_->stop(); 297   1401 impl_->stop();
HITCBC 298   1424 impl_->join(); 298   1401 impl_->join();
HITCBC 299   1424 shutdown(); 299   1401 shutdown();
300   // Takes what the unwinds post too, and the work of any service they 300   // Takes what the unwinds post too, and the work of any service they
301   // create, which is shut down as it is created. 301   // create, which is shut down as it is created.
HITCBC 302   1424 impl_->drain_abandoned(); 302   1401 impl_->drain_abandoned();
HITCBC 303   1424 destroy(); 303   1401 destroy();
304   // Services give up their work in shutdown(), but anything a 304   // Services give up their work in shutdown(), but anything a
305   // destructor posts anyway is still destroyed. 305   // destructor posts anyway is still destroyed.
HITCBC 306   1424 impl_->drain_abandoned(); 306   1401 impl_->drain_abandoned();
HITCBC 307   1424 delete impl_; 307   1401 delete impl_;
HITCBC 308   1424 } 308   1401 }
309   309  
HITCBC 310   1424 thread_pool:: 310   1401 thread_pool::
HITCBC 311   1424 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) 311   1401 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 312   1424 : impl_(new impl(num_threads, thread_name_prefix)) 312   1401 : impl_(new impl(num_threads, thread_name_prefix))
313   { 313   {
HITCBC 314   1424 this->set_frame_allocator(std::allocator<void>{}); 314   1401 this->set_frame_allocator(std::allocator<void>{});
HITCBC 315   1424 } 315   1401 }
316   316  
317   void 317   void
HITCBC 318   1281 thread_pool:: 318   1258 thread_pool::
319   join() noexcept 319   join() noexcept
320   { 320   {
HITCBC 321   1281 impl_->join(); 321   1258 impl_->join();
HITCBC 322   1281 } 322   1258 }
323   323  
324   void 324   void
HITCBC 325   303 thread_pool:: 325   303 thread_pool::
326   stop() noexcept 326   stop() noexcept
327   { 327   {
HITCBC 328   303 impl_->stop(); 328   303 impl_->stop();
HITCBC 329   303 } 329   303 }
330   330  
331   //------------------------------------------------------------------------------ 331   //------------------------------------------------------------------------------
332   332  
333   thread_pool::executor_type 333   thread_pool::executor_type
HITCBC 334   15341 thread_pool:: 334   15318 thread_pool::
335   get_executor() const noexcept 335   get_executor() const noexcept
336   { 336   {
HITCBC 337   15341 return executor_type( 337   15318 return executor_type(
HITCBC 338   15341 const_cast<thread_pool&>(*this)); 338   15318 const_cast<thread_pool&>(*this));
339   } 339   }
340   340  
341   void 341   void
HITCBC 342   4084 thread_pool::executor_type:: 342   4061 thread_pool::executor_type::
343   on_work_started() const noexcept 343   on_work_started() const noexcept
344   { 344   {
HITCBC 345   4084 pool_->impl_->on_work_started(); 345   4061 pool_->impl_->on_work_started();
HITCBC 346   4084 } 346   4061 }
347   347  
348   void 348   void
HITCBC 349   3784 thread_pool::executor_type:: 349   3761 thread_pool::executor_type::
350   on_work_finished() const noexcept 350   on_work_finished() const noexcept
351   { 351   {
HITCBC 352   3784 pool_->impl_->on_work_finished(); 352   3761 pool_->impl_->on_work_finished();
HITCBC 353   3784 } 353   3761 }
354   354  
355   void 355   void
HITCBC 356   19095 thread_pool::executor_type:: 356   20768 thread_pool::executor_type::
357   post(continuation& c) const 357   post(continuation& c) const
358   { 358   {
HITCBC 359   19095 pool_->impl_->post(c); 359   20768 pool_->impl_->post(c);
HITCBC 360   19095 } 360   20768 }
361   361  
362   std::coroutine_handle<> 362   std::coroutine_handle<>
HITCBC 363   3207 thread_pool::executor_type:: 363   3184 thread_pool::executor_type::
364   dispatch(continuation& c) const 364   dispatch(continuation& c) const
365   { 365   {
HITCBC 366   3207 if(pool_->impl_->running_in_this_thread()) 366   3184 if(pool_->impl_->running_in_this_thread())
HITCBC 367   7 return c.h; 367   7 return c.h;
HITCBC 368   3200 pool_->impl_->post(c); 368   3177 pool_->impl_->post(c);
HITCBC 369   3200 return std::noop_coroutine(); 369   3177 return std::noop_coroutine();
370   } 370   }
371   371  
372   } // capy 372   } // capy
373   } // boost 373   } // boost