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   21697 void push(continuation* c) noexcept 72   21233 void push(continuation* c) noexcept
73   { 73   {
HITCBC 74   21697 c->reserved = nullptr; 74   21233 c->reserved = nullptr;
HITCBC 75   21697 if(tail_) 75   21233 if(tail_)
HITCBC 76   7134 tail_->reserved = c; 76   6796 tail_->reserved = c;
77   else 77   else
HITCBC 78   14563 head_ = c; 78   14437 head_ = c;
HITCBC 79   21697 tail_ = c; 79   21233 tail_ = c;
HITCBC 80   21697 } 80   21233 }
81   81  
HITCBC 82   24563 continuation* pop() noexcept 82   24041 continuation* pop() noexcept
83   { 83   {
HITCBC 84   24563 if(!head_) 84   24041 if(!head_)
HITCBC 85   2866 return nullptr; 85   2808 return nullptr;
HITCBC 86   21697 continuation* c = head_; 86   21233 continuation* c = head_;
HITCBC 87   21697 head_ = static_cast<continuation*>(head_->reserved); 87   21233 head_ = static_cast<continuation*>(head_->reserved);
HITCBC 88   21697 if(!head_) 88   21233 if(!head_)
HITCBC 89   14563 tail_ = nullptr; 89   14437 tail_ = nullptr;
HITCBC 90   21697 return c; 90   21233 return c;
91   } 91   }
92   92  
HITCBC 93   35786 bool empty() const noexcept 93   35012 bool empty() const noexcept
94   { 94   {
HITCBC 95   35786 return head_ == nullptr; 95   35012 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   1433 ~impl() = default; 110   1404 ~impl() = default;
111   111  
112   bool 112   bool
HITCBC 113   3216 running_in_this_thread() const noexcept 113   3185 running_in_this_thread() const noexcept
114   { 114   {
HITCBC 115   3216 return current_.get() == this; 115   3185 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   2866 drain_abandoned() noexcept 122   2808 drain_abandoned() noexcept
123   { 123   {
124   for(;;) 124   for(;;)
125   { 125   {
126   continuation* c; 126   continuation* c;
127   { 127   {
HITCBC 128   3410 std::lock_guard<std::mutex> lock(mutex_); 128   3462 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 129   3410 c = pop(); 129   3462 c = pop();
HITCBC 130   3410 } 130   3462 }
HITCBC 131   3410 if(!c) 131   3462 if(!c)
HITCBC 132   2866 return; 132   2808 return;
HITCBC 133   544 if(c->h && c->h != std::noop_coroutine()) 133   654 if(c->h && c->h != std::noop_coroutine())
HITCBC 134   293 capy::abandon(*c); 134   403 capy::abandon(*c);
HITCBC 135   544 } 135   654 }
136   } 136   }
137   137  
HITCBC 138   1433 impl(std::size_t num_threads, std::string_view thread_name_prefix) 138   1404 impl(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 139   1433 : num_threads_(num_threads) 139   1404 : num_threads_(num_threads)
140   { 140   {
HITCBC 141   1433 if(num_threads_ == 0) 141   1404 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   1433 auto n = thread_name_prefix.copy(thread_name_prefix_, 12); 146   1404 auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
HITCBC 147   1433 thread_name_prefix_[n] = '\0'; 147   1404 thread_name_prefix_[n] = '\0';
HITCBC 148   1433 } 148   1404 }
149   149  
150   void 150   void
HITCBC 151   21697 post(continuation& c) 151   21233 post(continuation& c)
152   { 152   {
HITCBC 153   21697 if(!started_.load(std::memory_order_acquire)) 153   21233 if(!started_.load(std::memory_order_acquire))
154   { 154   {
155   { 155   {
HITCBC 156   598 std::lock_guard<std::mutex> lock(mutex_); 156   574 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 157   598 if(stop_) 157   574 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   219 push(&c);
HITCBC 162   220 return; 162   219 return;
163   } 163   }
HITCBC 164   598 } 164   574 }
HITCBC 165   378 ensure_started(); 165   355 ensure_started();
166   } 166   }
HITCBC 167   21477 std::lock_guard<std::mutex> lock(mutex_); 167   21014 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 168   21477 push(&c); 168   21014 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   21477 work_cv_.notify_one(); 171   21014 work_cv_.notify_one();
HITCBC 172   21477 } 172   21014 }
173   173  
174   void 174   void
HITCBC 175   4093 on_work_started() noexcept 175   4062 on_work_started() noexcept
176   { 176   {
HITCBC 177   4093 outstanding_work_.fetch_add(1, std::memory_order_acq_rel); 177   4062 outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
HITCBC 178   4093 } 178   4062 }
179   179  
180   void 180   void
HITCBC 181   3793 on_work_finished() noexcept 181   3762 on_work_finished() noexcept
182   { 182   {
HITCBC 183   3793 auto n = outstanding_work_.load(std::memory_order_relaxed); 183   3762 auto n = outstanding_work_.load(std::memory_order_relaxed);
HITCBC 184   3800 while(n > 1) 184   3763 while(n > 1)
185   { 185   {
HITCBC 186   906 if(outstanding_work_.compare_exchange_weak( 186   1262 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   446 return; 188   630 return;
189   } 189   }
HITCBC 190   3347 std::lock_guard<std::mutex> lock(mutex_); 190   3132 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 191   3347 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel); 191   3132 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   3347 if(outstanding_work_.load( 193   3132 if(outstanding_work_.load(
HITCBC 194   3347 std::memory_order_acquire) == 0 && joined_ && !stop_) 194   3132 std::memory_order_acquire) == 0 && joined_ && !stop_)
195   { 195   {
HITCBC 196   698 stop_ = true; 196   732 stop_ = true;
HITCBC 197   698 done_cv_.notify_all(); 197   732 done_cv_.notify_all();
HITCBC 198   698 work_cv_.notify_all(); 198   732 work_cv_.notify_all();
199   } 199   }
HITCBC 200   3347 } 200   3132 }
201   201  
202   void 202   void
HITCBC 203   2723 join() noexcept 203   2665 join() noexcept
204   { 204   {
205   { 205   {
HITCBC 206   2723 std::unique_lock<std::mutex> lock(mutex_); 206   2665 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 207   2723 if(joined_) 207   2665 if(joined_)
HITCBC 208   1290 return; 208   1261 return;
HITCBC 209   1433 joined_ = true; 209   1404 joined_ = true;
210   210  
HITCBC 211   1433 if(outstanding_work_.load( 211   1404 if(outstanding_work_.load(
HITCBC 212   1433 std::memory_order_acquire) == 0) 212   1404 std::memory_order_acquire) == 0)
213   { 213   {
HITCBC 214   379 stop_ = true; 214   317 stop_ = true;
HITCBC 215   379 work_cv_.notify_all(); 215   317 work_cv_.notify_all();
216   } 216   }
217   else 217   else
218   { 218   {
HITCBC 219   1054 done_cv_.wait(lock, [this]{ 219   1087 done_cv_.wait(lock, [this]{
HITCBC 220   2053 return stop_; 220   2120 return stop_;
221   }); 221   });
222   } 222   }
HITCBC 223   2723 } 223   2665 }
224   224  
HITCBC 225   1934 for(auto& t : threads_) 225   1880 for(auto& t : threads_)
HITCBC 226   501 if(t.joinable()) 226   476 if(t.joinable())
HITCBC 227   501 t.join(); 227   476 t.join();
228   } 228   }
229   229  
230   void 230   void
HITCBC 231   1736 stop() noexcept 231   1707 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   1736 std::lock_guard<std::mutex> lock(mutex_); 235   1707 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 236   1736 stop_ = true; 236   1707 stop_ = true;
HITCBC 237   1736 work_cv_.notify_all(); 237   1707 work_cv_.notify_all();
HITCBC 238   1736 done_cv_.notify_all(); 238   1707 done_cv_.notify_all();
HITCBC 239   1736 } 239   1707 }
240   240  
241   private: 241   private:
242   void 242   void
HITCBC 243   378 ensure_started() 243   355 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   378 std::lock_guard<std::mutex> lock(mutex_); 248   355 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 249   378 if(started_.load(std::memory_order_relaxed) || stop_) 249   355 if(started_.load(std::memory_order_relaxed) || stop_)
HITCBC 250   2 return; 250   7 return;
HITCBC 251   376 threads_.reserve(num_threads_); 251   348 threads_.reserve(num_threads_);
HITCBC 252   877 for(std::size_t i = 0; i < num_threads_; ++i) 252   824 for(std::size_t i = 0; i < num_threads_; ++i)
HITCBC 253   1002 threads_.emplace_back([this, i]{ run(i); }); 253   952 threads_.emplace_back([this, i]{ run(i); });
HITCBC 254   376 started_.store(true, std::memory_order_release); 254   348 started_.store(true, std::memory_order_release);
HITCBC 255   378 } 255   355 }
256   256  
257   void 257   void
HITCBC 258   501 run(std::size_t index) 258   476 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   501 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); 262   476 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
HITCBC 263   501 set_current_thread_name(name); 263   476 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   501 scoped_pool(impl const* p) noexcept { current_.set(p); } 269   476 scoped_pool(impl const* p) noexcept { current_.set(p); }
HITCBC 270   501 ~scoped_pool() noexcept { current_.set(nullptr); } 270   476 ~scoped_pool() noexcept { current_.set(nullptr); }
HITCBC 271   501 } guard(this); 271   476 } guard(this);
272   272  
273   for(;;) 273   for(;;)
274   { 274   {
HITCBC 275   21654 continuation* c = nullptr; 275   21055 continuation* c = nullptr;
276   { 276   {
HITCBC 277   21654 std::unique_lock<std::mutex> lock(mutex_); 277   21055 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 278   21654 work_cv_.wait(lock, [this]{ 278   21055 work_cv_.wait(lock, [this]{
HITCBC 279   50323 return !empty() || 279   49347 return !empty() ||
HITCBC 280   50323 stop_; 280   49347 stop_;
281   }); 281   });
HITCBC 282   21654 if(stop_) 282   21055 if(stop_)
HITCBC 283   1002 return; 283   952 return;
HITCBC 284   21153 c = pop(); 284   20579 c = pop();
HITCBC 285   21654 } 285   21055 }
HITCBC 286   21153 if(c) 286   20579 if(c)
HITCBC 287   21153 safe_resume(c->h); 287   20579 safe_resume(c->h);
HITCBC 288   21153 } 288   20579 }
HITCBC 289   501 } 289   476 }
290   }; 290   };
291   291  
292   //------------------------------------------------------------------------------ 292   //------------------------------------------------------------------------------
293   293  
HITCBC 294   1433 thread_pool:: 294   1404 thread_pool::
295   ~thread_pool() 295   ~thread_pool()
296   { 296   {
HITCBC 297   1433 impl_->stop(); 297   1404 impl_->stop();
HITCBC 298   1433 impl_->join(); 298   1404 impl_->join();
HITCBC 299   1433 shutdown(); 299   1404 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   1433 impl_->drain_abandoned(); 302   1404 impl_->drain_abandoned();
HITCBC 303   1433 destroy(); 303   1404 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   1433 impl_->drain_abandoned(); 306   1404 impl_->drain_abandoned();
HITCBC 307   1433 delete impl_; 307   1404 delete impl_;
HITCBC 308   1433 } 308   1404 }
309   309  
HITCBC 310   1433 thread_pool:: 310   1404 thread_pool::
HITCBC 311   1433 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) 311   1404 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 312   1433 : impl_(new impl(num_threads, thread_name_prefix)) 312   1404 : impl_(new impl(num_threads, thread_name_prefix))
313   { 313   {
HITCBC 314   1433 this->set_frame_allocator(std::allocator<void>{}); 314   1404 this->set_frame_allocator(std::allocator<void>{});
HITCBC 315   1433 } 315   1404 }
316   316  
317   void 317   void
HITCBC 318   1290 thread_pool:: 318   1261 thread_pool::
319   join() noexcept 319   join() noexcept
320   { 320   {
HITCBC 321   1290 impl_->join(); 321   1261 impl_->join();
HITCBC 322   1290 } 322   1261 }
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   15350 thread_pool:: 334   15321 thread_pool::
335   get_executor() const noexcept 335   get_executor() const noexcept
336   { 336   {
HITCBC 337   15350 return executor_type( 337   15321 return executor_type(
HITCBC 338   15350 const_cast<thread_pool&>(*this)); 338   15321 const_cast<thread_pool&>(*this));
339   } 339   }
340   340  
341   void 341   void
HITCBC 342   4093 thread_pool::executor_type:: 342   4062 thread_pool::executor_type::
343   on_work_started() const noexcept 343   on_work_started() const noexcept
344   { 344   {
HITCBC 345   4093 pool_->impl_->on_work_started(); 345   4062 pool_->impl_->on_work_started();
HITCBC 346   4093 } 346   4062 }
347   347  
348   void 348   void
HITCBC 349   3793 thread_pool::executor_type:: 349   3762 thread_pool::executor_type::
350   on_work_finished() const noexcept 350   on_work_finished() const noexcept
351   { 351   {
HITCBC 352   3793 pool_->impl_->on_work_finished(); 352   3762 pool_->impl_->on_work_finished();
HITCBC 353   3793 } 353   3762 }
354   354  
355   void 355   void
HITCBC 356   18488 thread_pool::executor_type:: 356   18055 thread_pool::executor_type::
357   post(continuation& c) const 357   post(continuation& c) const
358   { 358   {
HITCBC 359   18488 pool_->impl_->post(c); 359   18055 pool_->impl_->post(c);
HITCBC 360   18488 } 360   18055 }
361   361  
362   std::coroutine_handle<> 362   std::coroutine_handle<>
HITCBC 363   3216 thread_pool::executor_type:: 363   3185 thread_pool::executor_type::
364   dispatch(continuation& c) const 364   dispatch(continuation& c) const
365   { 365   {
HITCBC 366   3216 if(pool_->impl_->running_in_this_thread()) 366   3185 if(pool_->impl_->running_in_this_thread())
HITCBC 367   7 return c.h; 367   7 return c.h;
HITCBC 368   3209 pool_->impl_->post(c); 368   3178 pool_->impl_->post(c);
HITCBC 369   3209 return std::noop_coroutine(); 369   3178 return std::noop_coroutine();
370   } 370   }
371   371  
372   } // capy 372   } // capy
373   } // boost 373   } // boost