99.35% Lines (152/153) 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/frame_allocator.hpp> 15   #include <boost/capy/ex/frame_allocator.hpp>
15   #include <boost/capy/test/thread_name.hpp> 16   #include <boost/capy/test/thread_name.hpp>
16   #include <algorithm> 17   #include <algorithm>
17   #include <atomic> 18   #include <atomic>
18   #include <condition_variable> 19   #include <condition_variable>
19   #include <cstdio> 20   #include <cstdio>
20   #include <mutex> 21   #include <mutex>
21   #include <thread> 22   #include <thread>
22   #include <vector> 23   #include <vector>
23   24  
24   /* 25   /*
25   Thread pool implementation using a shared work queue. 26   Thread pool implementation using a shared work queue.
26   27  
27   Work items are continuations linked via their intrusive next pointer, 28   Work items are continuations linked via their intrusive next pointer,
28   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
29   allocation: the continuation is owned by the caller and linked 30   allocation: the continuation is owned by the caller and linked
30   directly. Worker threads wait on a condition_variable until work 31   directly. Worker threads wait on a condition_variable until work
31   is available or stop is requested. 32   is available or stop is requested.
32   33  
33 - Threads are started lazily on first post() via std::call_once to avoid 34 + Threads are started lazily on first post(), under mutex_, to avoid
34 - spawning threads for pools that are constructed but never used. Each 35 + spawning threads for pools that are constructed but never used. A
35 - thread is named with a configurable prefix plus index for debugger 36 + stopped pool starts none. Each thread is named
36 - visibility. 37 + with a configurable prefix plus index for debugger visibility.
37   38  
38   Work tracking: on_work_started/on_work_finished maintain the atomic 39   Work tracking: on_work_started/on_work_finished maintain the atomic
39 - outstanding_work_ counter. on_work_started is lock-free; the worker 40 + outstanding_work_ counter. on_work_started is lock-free, and so is
40 - that drives the count to zero takes mutex_ and re-reads the count 41 + on_work_finished while the count stays above one. The release that
41 - before deciding to stop, so the count and the stop decision stay 42 + may take it to zero is made under mutex_, so join() can't see zero,
42 - consistent even if work is started in between. join() blocks until 43 + return and let the pool be destroyed while that call still uses it.
43 - this counter reaches zero, then signals workers to stop and joins 44 + join() blocks until this counter reaches zero, then signals workers
44 - threads. 45 + to stop and joins threads.
45   46  
46   Two shutdown paths: 47   Two shutdown paths:
47   - join(): waits for outstanding work to drain, then stops workers. 48   - join(): waits for outstanding work to drain, then stops workers.
48 - - stop(): immediately signals workers to exit; queued work is abandoned. 49 + - stop(): immediately signals workers to exit; queued work stays queued.
49 - - Destructor: stop() then join() (abandon + wait for threads). 50 + - Destructor: stop() then join(), then queued work is abandoned.
50   */ 51   */
51   52  
52   namespace boost { 53   namespace boost {
53   namespace capy { 54   namespace capy {
54   55  
55   //------------------------------------------------------------------------------ 56   //------------------------------------------------------------------------------
56   57  
57   class thread_pool::impl 58   class thread_pool::impl
58   { 59   {
59   // Identifies the pool owning the current worker thread, or 60   // Identifies the pool owning the current worker thread, or
60   // nullptr if the calling thread is not a pool worker. Checked 61   // nullptr if the calling thread is not a pool worker. Checked
61   // by dispatch() to decide between symmetric transfer (inline 62   // by dispatch() to decide between symmetric transfer (inline
62   // resume) and post. 63   // resume) and post.
63   static inline detail::thread_local_ptr<impl const> current_; 64   static inline detail::thread_local_ptr<impl const> current_;
64   65  
65   // Intrusive queue of continuations: the next link is stored in 66   // Intrusive queue of continuations: the next link is stored in
66   // continuation::reserved (typed continuation* round-tripped through 67   // continuation::reserved (typed continuation* round-tripped through
67   // 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.
68   continuation* head_ = nullptr; 69   continuation* head_ = nullptr;
69   continuation* tail_ = nullptr; 70   continuation* tail_ = nullptr;
70   71  
HITCBC 71   19997 void push(continuation* c) noexcept 72   24506 void push(continuation* c) noexcept
72   { 73   {
HITCBC 73   19997 c->reserved = nullptr; 74   24506 c->reserved = nullptr;
HITCBC 74   19997 if(tail_) 75   24506 if(tail_)
HITCBC 75   1685 tail_->reserved = c; 76   4625 tail_->reserved = c;
76   else 77   else
HITCBC 77   18312 head_ = c; 78   19881 head_ = c;
HITCBC 78   19997 tail_ = c; 79   24506 tail_ = c;
HITCBC 79   19997 } 80   24506 }
80   81  
HITCBC 81   20397 continuation* pop() noexcept 82   27378 continuation* pop() noexcept
82   { 83   {
HITCBC 83   20397 if(!head_) 84   27378 if(!head_)
HITCBC 84   400 return nullptr; 85   2872 return nullptr;
HITCBC 85   19997 continuation* c = head_; 86   24506 continuation* c = head_;
HITCBC 86   19997 head_ = static_cast<continuation*>(head_->reserved); 87   24506 head_ = static_cast<continuation*>(head_->reserved);
HITCBC 87   19997 if(!head_) 88   24506 if(!head_)
HITCBC 88   18312 tail_ = nullptr; 89   19881 tail_ = nullptr;
HITCBC 89   19997 return c; 90   24506 return c;
90   } 91   }
91   92  
HITCBC 92   38879 bool empty() const noexcept 93   42696 bool empty() const noexcept
93   { 94   {
HITCBC 94   38879 return head_ == nullptr; 95   42696 return head_ == nullptr;
95   } 96   }
96   97  
97   std::mutex mutex_; 98   std::mutex mutex_;
98   std::condition_variable work_cv_; 99   std::condition_variable work_cv_;
99   std::condition_variable done_cv_; 100   std::condition_variable done_cv_;
100   std::vector<std::thread> threads_; 101   std::vector<std::thread> threads_;
101   std::atomic<std::size_t> outstanding_work_{0}; 102   std::atomic<std::size_t> outstanding_work_{0};
102   bool stop_{false}; 103   bool stop_{false};
103   bool joined_{false}; 104   bool joined_{false};
104   std::size_t num_threads_; 105   std::size_t num_threads_;
105   char thread_name_prefix_[13]{}; // 12 chars max + null terminator 106   char thread_name_prefix_[13]{}; // 12 chars max + null terminator
106 - std::once_flag start_flag_; 107 + std::atomic<bool> started_{false};
107   108  
108   public: 109   public:
HITCBC 109   400 ~impl() = default; 110   1434 ~impl() = default;
110   111  
111   bool 112   bool
HITCBC 112   685 running_in_this_thread() const noexcept 113   2718 running_in_this_thread() const noexcept
113   { 114   {
HITCBC 114   685 return current_.get() == this; 115   2718 return current_.get() == this;
115   } 116   }
116   117  
117 - // Destroy abandoned coroutine frames. Must be called 118 + // Abandon queued coroutines. Called after the services shut down,
118 - // before execution_context::shutdown()/destroy() so 119 + // since their shutdown can post more, and again after they are
119 - // that suspended-frame destructors touching services 120 + // destroyed, for anything their destructors post. Each is taken
120 - // (e.g. cancelling registrations) run while those 121 + // under the lock, as other threads may still post, and abandoned
121 - // services are still valid. 122 + // outside it, as abandoning can post. Returns whether there was
122 - void 123 + // anything to abandon.
  124 + bool
HITCBC 123   400 drain_abandoned() noexcept 125   2872 drain_abandoned() noexcept
124   { 126   {
HITCBC 125 - 602 while(auto* c = pop()) 127 + 2872 bool any = false;
  128 + for(;;)
126   { 129   {
ECB 127 - 202 auto h = c->h; 130 + continuation* c;
ECB 128 - 202 if(h && h != std::noop_coroutine()) 131 + {
HITCBC 129 - 151 h.destroy(); 132 + 5375 std::lock_guard<std::mutex> lock(mutex_);
HITGNC   133 + 5375 c = pop();
HITGNC   134 + 5375 }
HITGNC   135 + 5375 if(!c)
HITGNC   136 + 2872 return any;
HITGNC   137 + 2503 any = true;
HITGNC   138 + 2503 if(c->h && c->h != std::noop_coroutine())
HITGNC   139 + 2252 capy::abandon(*c);
HITCBC 130   202 } 140   2503 }
ECB 131   400 } 141   }
132   142  
HITCBC 133   400 impl(std::size_t num_threads, std::string_view thread_name_prefix) 143   1434 impl(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 134   400 : num_threads_(num_threads) 144   1434 : num_threads_(num_threads)
135   { 145   {
HITCBC 136   400 if(num_threads_ == 0) 146   1434 if(num_threads_ == 0)
HITCBC 137   8 num_threads_ = std::max( 147   8 num_threads_ = std::max(
HITCBC 138   4 std::thread::hardware_concurrency(), 1u); 148   4 std::thread::hardware_concurrency(), 1u);
139   149  
140   // Truncate prefix to 12 chars, leaving room for up to 3-digit index. 150   // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
HITCBC 141   400 auto n = thread_name_prefix.copy(thread_name_prefix_, 12); 151   1434 auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
HITCBC 142   400 thread_name_prefix_[n] = '\0'; 152   1434 thread_name_prefix_[n] = '\0';
HITCBC 143   400 } 153   1434 }
144   154  
145   void 155   void
HITCBC 146   19997 post(continuation& c) 156   24506 post(continuation& c)
147   { 157   {
HITCBC 148 - 19997 ensure_started(); 158 + 24506 if(!started_.load(std::memory_order_acquire))
149   { 159   {
ECB 150 - 19997 std::lock_guard<std::mutex> lock(mutex_); 160 + {
HITCBC 151 - 19997 push(&c); 161 + 2584 std::lock_guard<std::mutex> lock(mutex_);
HITGIC 152 - // Under the lock so the pool cannot drain, join, and 162 + 2584 if(stop_)
153 - // destroy the condition variable mid-signal. 163 + {
ECB 154 - 19997 work_cv_.notify_one(); 164 + // Stopped before any thread started: nothing will
  165 + // run it, and the destructor's drain abandons it.
HITGNC   166 + 2204 push(&c);
HITGNC   167 + 2204 return;
  168 + }
HITGNC   169 + 2584 }
HITGNC   170 + 380 ensure_started();
ECB 155   19997 } 171   }
HITGNC   172 + 22302 std::lock_guard<std::mutex> lock(mutex_);
HITGNC   173 + 22302 push(&c);
  174 + // Under the lock so the pool cannot drain, join, and
  175 + // destroy the condition variable mid-signal.
HITGNC   176 + 22302 work_cv_.notify_one();
HITCBC 156   19997 } 177   22302 }
157   178  
158   void 179   void
HITCBC 159   684 on_work_started() noexcept 180   3595 on_work_started() noexcept
160   { 181   {
HITCBC 161   684 outstanding_work_.fetch_add(1, std::memory_order_acq_rel); 182   3595 outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
HITCBC 162   684 } 183   3595 }
163   184  
164   void 185   void
HITCBC 165   684 on_work_finished() noexcept 186   3295 on_work_finished() noexcept
166   { 187   {
HITCBC 167 - 684 if(outstanding_work_.fetch_sub( 188 + 3295 auto n = outstanding_work_.load(std::memory_order_relaxed);
HITCBC 168 - 684 1, std::memory_order_acq_rel) == 1) 189 + 3305 while(n > 1)
169   { 190   {
HITGIC 170 - // fetch_sub's result can be stale: a concurrent 191 + 906 if(outstanding_work_.compare_exchange_weak(
171 - // on_work_started() may raise the count before we take the 192 + n, n - 1, std::memory_order_acq_rel))
HITGIC 172 - // lock, so re-read it here rather than trust the decrement. 193 + 443 return;
ECB 173 - 321 std::lock_guard<std::mutex> lock(mutex_); 194 + }
HITCBC 174 - 321 if(outstanding_work_.load( 195 + 2852 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 175 - 321 std::memory_order_acquire) == 0 && joined_ && !stop_) 196 + 2852 outstanding_work_.fetch_sub(1, std::memory_order_acq_rel);
176 - { 197 + // Re-read: a concurrent on_work_started() may have raised it.
HITCBC 177 - 168 stop_ = true; 198 + 2852 if(outstanding_work_.load(
HITCBC 178 - 168 done_cv_.notify_all(); 199 + 2852 std::memory_order_acquire) == 0 && joined_ && !stop_)
ECB 179 - 168 work_cv_.notify_all(); 200 + {
HITGIC 180 - } 201 + 703 stop_ = true;
HITGNC   202 + 703 done_cv_.notify_all();
HITGNC   203 + 703 work_cv_.notify_all();
ECB 181   321 } 204   }
HITCBC 182   684 } 205   2852 }
183   206  
184   void 207   void
HITCBC 185   663 join() noexcept 208   2728 join() noexcept
186   { 209   {
187   { 210   {
HITCBC 188   663 std::unique_lock<std::mutex> lock(mutex_); 211   2728 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 189   663 if(joined_) 212   2728 if(joined_)
HITCBC 190   263 return; 213   1294 return;
HITCBC 191   400 joined_ = true; 214   1434 joined_ = true;
192   215  
HITCBC 193   400 if(outstanding_work_.load( 216   1434 if(outstanding_work_.load(
HITCBC 194   400 std::memory_order_acquire) == 0) 217   1434 std::memory_order_acquire) == 0)
195   { 218   {
HITCBC 196   179 stop_ = true; 219   376 stop_ = true;
HITCBC 197   179 work_cv_.notify_all(); 220   376 work_cv_.notify_all();
198   } 221   }
199   else 222   else
200   { 223   {
HITCBC 201   221 done_cv_.wait(lock, [this]{ 224   1058 done_cv_.wait(lock, [this]{
HITCBC 202   390 return stop_; 225   2062 return stop_;
203   }); 226   });
204   } 227   }
HITCBC 205   663 } 228   2728 }
206   229  
HITCBC 207   851 for(auto& t : threads_) 230   1938 for(auto& t : threads_)
HITCBC 208   451 if(t.joinable()) 231   504 if(t.joinable())
HITCBC 209   451 t.join(); 232   504 t.join();
210   } 233   }
211   234  
212   void 235   void
HITCBC 213   402 stop() noexcept 236   1737 stop() noexcept
214   { 237   {
215 - { 238 + // Under the lock: the join() this releases may return and let
ECB 216 - 402 std::lock_guard<std::mutex> lock(mutex_); 239 + // the pool be destroyed as soon as the lock is free.
HITCBC 217 - 402 stop_ = true; 240 + 1737 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 218 - 402 } 241 + 1737 stop_ = true;
HITCBC 219   402 work_cv_.notify_all(); 242   1737 work_cv_.notify_all();
HITCBC 220   402 done_cv_.notify_all(); 243   1737 done_cv_.notify_all();
HITCBC 221   402 } 244   1737 }
222   245  
223   private: 246   private:
224   void 247   void
HITCBC 225   19997 ensure_started() 248   380 ensure_started()
226   { 249   {
ECB 227 - 19997 std::call_once(start_flag_, [this]{ 250 + // Under the lock, so join() either sees every thread started
ECB 228 - 347 threads_.reserve(num_threads_); 251 + // here or makes this start none. A joined pool still starts them:
ECB 229 - 798 for(std::size_t i = 0; i < num_threads_; ++i) 252 + // join() may be waiting on work that needs them.
HITCBC 230 - 902 threads_.emplace_back([this, i]{ run(i); }); 253 + 380 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 231 - 347 }); 254 + 380 if(started_.load(std::memory_order_relaxed) || stop_)
MISUNC   255 + ✗ return;
HITGNC   256 + 380 threads_.reserve(num_threads_);
HITGNC   257 + 884 for(std::size_t i = 0; i < num_threads_; ++i)
HITGNC   258 + 1008 threads_.emplace_back([this, i]{ run(i); });
HITGNC   259 + 380 started_.store(true, std::memory_order_release);
HITCBC 232   19997 } 260   380 }
233   261  
234   void 262   void
HITCBC 235   451 run(std::size_t index) 263   504 run(std::size_t index)
236   { 264   {
237   // Build name; set_current_thread_name truncates to platform limits. 265   // Build name; set_current_thread_name truncates to platform limits.
238   char name[16]; 266   char name[16];
HITCBC 239   451 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index); 267   504 std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
HITCBC 240   451 set_current_thread_name(name); 268   504 set_current_thread_name(name);
241   269  
242   // Mark this thread as a worker of this pool so dispatch() 270   // Mark this thread as a worker of this pool so dispatch()
243   // can symmetric-transfer when called from within pool work. 271   // can symmetric-transfer when called from within pool work.
244   struct scoped_pool 272   struct scoped_pool
245   { 273   {
HITCBC 246   451 scoped_pool(impl const* p) noexcept { current_.set(p); } 274   504 scoped_pool(impl const* p) noexcept { current_.set(p); }
HITCBC 247   451 ~scoped_pool() noexcept { current_.set(nullptr); } 275   504 ~scoped_pool() noexcept { current_.set(nullptr); }
HITCBC 248   451 } guard(this); 276   504 } guard(this);
249   277  
250   for(;;) 278   for(;;)
251   { 279   {
HITCBC 252   20246 continuation* c = nullptr; 280   22507 continuation* c = nullptr;
253   { 281   {
HITCBC 254   20246 std::unique_lock<std::mutex> lock(mutex_); 282   22507 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 255   20246 work_cv_.wait(lock, [this]{ 283   22507 work_cv_.wait(lock, [this]{
HITCBC 256   57872 return !empty() || 284   63302 return !empty() ||
HITCBC 257   57872 stop_; 285   63302 stop_;
258   }); 286   });
HITCBC 259   20246 if(stop_) 287   22507 if(stop_)
HITCBC 260   902 return; 288   1008 return;
HITCBC 261   19795 c = pop(); 289   22003 c = pop();
HITCBC 262   20246 } 290   22507 }
HITCBC 263   19795 if(c) 291   22003 if(c)
HITCBC 264   19795 safe_resume(c->h); 292   22003 safe_resume(c->h);
HITCBC 265   19795 } 293   22003 }
HITCBC 266   451 } 294   504 }
267   }; 295   };
268   296  
269   //------------------------------------------------------------------------------ 297   //------------------------------------------------------------------------------
270   298  
HITCBC 271   400 thread_pool:: 299   1434 thread_pool::
272   ~thread_pool() 300   ~thread_pool()
273   { 301   {
HITCBC 274   400 impl_->stop(); 302   1434 impl_->stop();
DCB 275 - 400 impl_->drain_abandoned();  
HITCBC 276   400 impl_->join(); 303   1434 impl_->join();
HITCBC 277   400 shutdown(); 304   1434 shutdown();
HITCBC 278 - 400 destroy(); 305 + 1434 impl_->drain_abandoned();
  306 + // A frame the drain unwinds can create a service, whose destructor
  307 + // can post again: repeat until nothing is left of either.
  308 + do
  309 + {
HITGNC   310 + 1438 destroy();
  311 + }
HITGNC   312 + 1438 while(impl_->drain_abandoned());
HITCBC 279   400 delete impl_; 313   1434 delete impl_;
HITCBC 280   400 } 314   1434 }
281   315  
HITCBC 282   400 thread_pool:: 316   1434 thread_pool::
HITCBC 283   400 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix) 317   1434 thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
HITCBC 284   400 : impl_(new impl(num_threads, thread_name_prefix)) 318   1434 : impl_(new impl(num_threads, thread_name_prefix))
285   { 319   {
HITCBC 286   400 this->set_frame_allocator(std::allocator<void>{}); 320   1434 this->set_frame_allocator(std::allocator<void>{});
HITCBC 287   400 } 321   1434 }
288   322  
289   void 323   void
HITCBC 290   263 thread_pool:: 324   1294 thread_pool::
291   join() noexcept 325   join() noexcept
292   { 326   {
HITCBC 293   263 impl_->join(); 327   1294 impl_->join();
HITCBC 294   263 } 328   1294 }
295   329  
296   void 330   void
HITCBC 297   2 thread_pool:: 331   303 thread_pool::
298   stop() noexcept 332   stop() noexcept
299   { 333   {
HITCBC 300   2 impl_->stop(); 334   303 impl_->stop();
HITCBC 301   2 } 335   303 }
302   336  
303   //------------------------------------------------------------------------------ 337   //------------------------------------------------------------------------------
304   338  
305   thread_pool::executor_type 339   thread_pool::executor_type
HITCBC 306   11814 thread_pool:: 340   14849 thread_pool::
307   get_executor() const noexcept 341   get_executor() const noexcept
308   { 342   {
HITCBC 309   11814 return executor_type( 343   14849 return executor_type(
HITCBC 310   11814 const_cast<thread_pool&>(*this)); 344   14849 const_cast<thread_pool&>(*this));
311   } 345   }
312   346  
313   void 347   void
HITCBC 314   684 thread_pool::executor_type:: 348   3595 thread_pool::executor_type::
315   on_work_started() const noexcept 349   on_work_started() const noexcept
316   { 350   {
HITCBC 317   684 pool_->impl_->on_work_started(); 351   3595 pool_->impl_->on_work_started();
HITCBC 318   684 } 352   3595 }
319   353  
320   void 354   void
HITCBC 321   684 thread_pool::executor_type:: 355   3295 thread_pool::executor_type::
322   on_work_finished() const noexcept 356   on_work_finished() const noexcept
323   { 357   {
HITCBC 324   684 pool_->impl_->on_work_finished(); 358   3295 pool_->impl_->on_work_finished();
HITCBC 325   684 } 359   3295 }
326   360  
327   void 361   void
HITCBC 328   19319 thread_pool::executor_type:: 362   21795 thread_pool::executor_type::
329   post(continuation& c) const 363   post(continuation& c) const
330   { 364   {
HITCBC 331   19319 pool_->impl_->post(c); 365   21795 pool_->impl_->post(c);
HITCBC 332   19319 } 366   21795 }
333   367  
334   std::coroutine_handle<> 368   std::coroutine_handle<>
HITCBC 335   685 thread_pool::executor_type:: 369   2718 thread_pool::executor_type::
336   dispatch(continuation& c) const 370   dispatch(continuation& c) const
337   { 371   {
HITCBC 338   685 if(pool_->impl_->running_in_this_thread()) 372   2718 if(pool_->impl_->running_in_this_thread())
HITCBC 339   7 return c.h; 373   7 return c.h;
HITCBC 340   678 pool_->impl_->post(c); 374   2711 pool_->impl_->post(c);
HITCBC 341   678 return std::noop_coroutine(); 375   2711 return std::noop_coroutine();
342   } 376   }
343   377  
344   } // capy 378   } // capy
345   } // boost 379   } // boost