98.67% Lines (74/75) 100.00% Functions (11/11)
TLA Baseline Branch
Line Hits Code Line Hits Code
1   // 1   //
2   // Copyright (c) 2026 Steve Gerbino 2   // Copyright (c) 2026 Steve Gerbino
3   // 3   //
4   // Distributed under the Boost Software License, Version 1.0. (See accompanying 4   // Distributed under the Boost Software License, Version 1.0. (See accompanying
5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 5   // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6   // 6   //
7   // Official repository: https://github.com/cppalliance/corosio 7   // Official repository: https://github.com/cppalliance/corosio
8   // 8   //
9   9  
10   #ifndef BOOST_COROSIO_DETAIL_THREAD_POOL_HPP 10   #ifndef BOOST_COROSIO_DETAIL_THREAD_POOL_HPP
11   #define BOOST_COROSIO_DETAIL_THREAD_POOL_HPP 11   #define BOOST_COROSIO_DETAIL_THREAD_POOL_HPP
12   12  
13   #include <boost/corosio/detail/config.hpp> 13   #include <boost/corosio/detail/config.hpp>
14   #include <boost/corosio/detail/intrusive.hpp> 14   #include <boost/corosio/detail/intrusive.hpp>
15   #include <boost/capy/error.hpp> 15   #include <boost/capy/error.hpp>
16   #include <boost/capy/ex/execution_context.hpp> 16   #include <boost/capy/ex/execution_context.hpp>
17   #include <boost/capy/test/thread_name.hpp> 17   #include <boost/capy/test/thread_name.hpp>
18   18  
19   #include <atomic> 19   #include <atomic>
20   #include <condition_variable> 20   #include <condition_variable>
21   #include <cstdio> 21   #include <cstdio>
22   #include <mutex> 22   #include <mutex>
23   #include <stdexcept> 23   #include <stdexcept>
24   #include <system_error> 24   #include <system_error>
25   #include <thread> 25   #include <thread>
26   #include <vector> 26   #include <vector>
27   27  
28   namespace boost::corosio::detail { 28   namespace boost::corosio::detail {
29   29  
30   /** Base class for thread pool work items. 30   /** Base class for thread pool work items.
31   31  
32   Derive from this to create work that can be posted to a 32   Derive from this to create work that can be posted to a
33   @ref thread_pool. Uses static function pointer dispatch, 33   @ref thread_pool. Uses static function pointer dispatch,
34   consistent with the IOCP `op` pattern. 34   consistent with the IOCP `op` pattern.
35   35  
36   @par Example 36   @par Example
37   @code 37   @code
38   struct my_work : pool_work_item 38   struct my_work : pool_work_item
39   { 39   {
40   int* result; 40   int* result;
41   static void execute( pool_work_item* w ) noexcept 41   static void execute( pool_work_item* w ) noexcept
42   { 42   {
43   auto* self = static_cast<my_work*>( w ); 43   auto* self = static_cast<my_work*>( w );
44   *self->result = 42; 44   *self->result = 42;
45   } 45   }
46   }; 46   };
47   47  
48   my_work w; 48   my_work w;
49   w.func_ = &my_work::execute; 49   w.func_ = &my_work::execute;
50   w.result = &r; 50   w.result = &r;
51   auto ec = pool.post( &w ); 51   auto ec = pool.post( &w );
52   @endcode 52   @endcode
53   */ 53   */
54   struct pool_work_item : intrusive_queue<pool_work_item>::node 54   struct pool_work_item : intrusive_queue<pool_work_item>::node
55   { 55   {
56   /// Static dispatch function signature. 56   /// Static dispatch function signature.
57   using func_type = void (*)(pool_work_item*) noexcept; 57   using func_type = void (*)(pool_work_item*) noexcept;
58   58  
59   /// Completion handler invoked by the worker thread. 59   /// Completion handler invoked by the worker thread.
60   func_type func_ = nullptr; 60   func_type func_ = nullptr;
61   }; 61   };
62   62  
63   /** Shared thread pool for dispatching blocking operations. 63   /** Shared thread pool for dispatching blocking operations.
64   64  
65   Provides a fixed pool of reusable worker threads for operations 65   Provides a fixed pool of reusable worker threads for operations
66   that cannot be integrated with async I/O (e.g. blocking DNS 66   that cannot be integrated with async I/O (e.g. blocking DNS
67   calls). Registered as an `execution_context::service` so it 67   calls). Registered as an `execution_context::service` so it
68   is a singleton per io_context. 68   is a singleton per io_context.
69   69  
70   The service is created with its context, but the workers start on 70   The service is created with its context, but the workers start on
71   the first `post()`: a context that never opens a file and never 71   the first `post()`: a context that never opens a file and never
72   resolves a name never pays for a thread. The default thread count 72   resolves a name never pays for a thread. The default thread count
73   is 1. 73   is 1.
74   74  
75   @par Thread Safety 75   @par Thread Safety
76   All public member functions are thread-safe. 76   All public member functions are thread-safe.
77   77  
78   @par Shutdown 78   @par Shutdown
79   Sets a shutdown flag, notifies all threads, and joins them. 79   Sets a shutdown flag, notifies all threads, and joins them.
80   In-flight blocking calls complete naturally before the thread 80   In-flight blocking calls complete naturally before the thread
81   exits. 81   exits.
82   82  
83   @note Create this service after the scheduler its work items post 83   @note Create this service after the scheduler its work items post
84   completions to. Services shut down newest first, so a pool created 84   completions to. Services shut down newest first, so a pool created
85   earlier joins its workers only after the scheduler has drained its 85   earlier joins its workers only after the scheduler has drained its
86   completion queue, and the completion the last worker posts is then 86   completion queue, and the completion the last worker posts is then
87   neither run nor destroyed. 87   neither run nor destroyed.
88   88  
89   @note The type is symbol-visible because services are keyed by type 89   @note The type is symbol-visible because services are keyed by type
90   identity: with RTTI, hidden behind a shared library boundary, a 90   identity: with RTTI, hidden behind a shared library boundary, a
91   module that asks for the pool would look up, and create, one of its 91   module that asks for the pool would look up, and create, one of its
92   own (the no-RTTI key is a template static whose visibility follows 92   own (the no-RTTI key is a template static whose visibility follows
93   the template it is instantiated from). 93   the template it is instantiated from).
94   */ 94   */
95   class BOOST_COROSIO_SYMBOL_VISIBLE thread_pool final 95   class BOOST_COROSIO_SYMBOL_VISIBLE thread_pool final
96   : public capy::execution_context::service 96   : public capy::execution_context::service
97   { 97   {
98   std::mutex mutex_; 98   std::mutex mutex_;
99   std::condition_variable cv_; 99   std::condition_variable cv_;
100   intrusive_queue<pool_work_item> work_queue_; 100   intrusive_queue<pool_work_item> work_queue_;
101   std::vector<std::thread> threads_; 101   std::vector<std::thread> threads_;
102   unsigned num_threads_; 102   unsigned num_threads_;
103   bool shutdown_ = false; 103   bool shutdown_ = false;
104   104  
105   void worker_loop(unsigned index); 105   void worker_loop(unsigned index);
106   std::error_code start_workers() noexcept; 106   std::error_code start_workers() noexcept;
107   107  
108   public: 108   public:
109   using key_type = thread_pool; 109   using key_type = thread_pool;
110   110  
111   /** Construct the thread pool service. 111   /** Construct the thread pool service.
112   112  
113   Records the worker count. The workers themselves start on the 113   Records the worker count. The workers themselves start on the
114   first `post()`. 114   first `post()`.
115   115  
116   @par Exception Safety 116   @par Exception Safety
117   Strong guarantee. 117   Strong guarantee.
118   118  
119   @param ctx Reference to the owning execution_context. 119   @param ctx Reference to the owning execution_context.
120   @param num_threads Number of worker threads. Must be 120   @param num_threads Number of worker threads. Must be
121   at least 1. 121   at least 1.
122   122  
123   @throws std::logic_error If `num_threads` is 0. 123   @throws std::logic_error If `num_threads` is 0.
124   */ 124   */
HITCBC 125   2109 explicit thread_pool( 125   2109 explicit thread_pool(
126 - [[maybe_unused]] capy::execution_context& ctx, 126 + [[maybe_unused]] capy::execution_context& ctx, unsigned num_threads = 1)
127 - unsigned num_threads = 1)  
HITCBC 128   2109 : num_threads_(num_threads) 127   2109 : num_threads_(num_threads)
129   { 128   {
HITCBC 130   2109 if (!num_threads) 129   2109 if (!num_threads)
HITCBC 131   1 throw std::logic_error("thread_pool requires at least 1 thread"); 130   1 throw std::logic_error("thread_pool requires at least 1 thread");
HITCBC 132   2111 } 131   2111 }
133   132  
134   /** Destroy the pool, joining any worker `shutdown()` never reached. 133   /** Destroy the pool, joining any worker `shutdown()` never reached.
135   134  
136   The context's shutdown walk is the normal path; this only 135   The context's shutdown walk is the normal path; this only
137   catches a pool created after that walk, whose `shutdown()` is 136   catches a pool created after that walk, whose `shutdown()` is
138   therefore never called and whose joinable threads would 137   therefore never called and whose joinable threads would
139   otherwise terminate the process. A pool that was never posted 138   otherwise terminate the process. A pool that was never posted
140   to holds no thread and needs neither. 139   to holds no thread and needs neither.
141   */ 140   */
HITCBC 142   4214 ~thread_pool() override 141   4214 ~thread_pool() override
HITCBC 143   2108 { 142   2108 {
HITCBC 144   2108 if (!threads_.empty()) 143   2108 if (!threads_.empty())
HITCBC 145   1 shutdown(); 144   1 shutdown();
HITCBC 146   4214 } 145   4214 }
147   146  
148   thread_pool(thread_pool const&) = delete; 147   thread_pool(thread_pool const&) = delete;
149   thread_pool& operator=(thread_pool const&) = delete; 148   thread_pool& operator=(thread_pool const&) = delete;
150   149  
151   /** Enqueue a work item for execution on the thread pool. 150   /** Enqueue a work item for execution on the thread pool.
152   151  
153   The first item posted starts the workers. Zero-allocation: 152   The first item posted starts the workers. Zero-allocation:
154   the caller owns the work item's storage. 153   the caller owns the work item's storage.
155   154  
156   A refusal answers with the code the caller reports for the 155   A refusal answers with the code the caller reports for the
157   operation it was starting, so that a system that will not give 156   operation it was starting, so that a system that will not give
158   the pool a thread is not mistaken for a cancellation. 157   the pool a thread is not mistaken for a cancellation.
159   158  
160   @par Thread Safety 159   @par Thread Safety
161   Safe. Racing first posts start the workers once. 160   Safe. Racing first posts start the workers once.
162   161  
163   @param w The work item to execute. Must remain valid until 162   @param w The work item to execute. Must remain valid until
164   its `func_` has been called. 163   its `func_` has been called.
165   164  
166   @return An empty code if the item was enqueued; 165   @return An empty code if the item was enqueued;
167   `capy::error::canceled` if the pool has already shut 166   `capy::error::canceled` if the pool has already shut
168   down; otherwise the code of the thread the system 167   down; otherwise the code of the thread the system
169   refused, which left the pool with no worker at all. 168   refused, which left the pool with no worker at all.
170   */ 169   */
171   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept; 170   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept;
172   171  
173   /** Return the number of workers the pool has started. 172   /** Return the number of workers the pool has started.
174   173  
175   Zero until the first `post()`, and zero again once 174   Zero until the first `post()`, and zero again once
176   `shutdown()` has joined them. 175   `shutdown()` has joined them.
177   176  
178   @par Thread Safety 177   @par Thread Safety
179   Safe. 178   Safe.
180   */ 179   */
HITCBC 181   6 unsigned worker_count() noexcept 180   6 unsigned worker_count() noexcept
182   { 181   {
HITCBC 183   6 std::lock_guard<std::mutex> lock(mutex_); 182   6 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 184   6 return static_cast<unsigned>(threads_.size()); 183   6 return static_cast<unsigned>(threads_.size());
HITCBC 185   6 } 184   6 }
186   185  
187   /** Shut down the thread pool. 186   /** Shut down the thread pool.
188   187  
189   Signals all threads to exit after draining any 188   Signals all threads to exit after draining any
190   remaining queued work, then joins them. 189   remaining queued work, then joins them.
191   */ 190   */
192   void shutdown() override; 191   void shutdown() override;
193   }; 192   };
194   193  
195   inline void 194   inline void
HITCBC 196   185 thread_pool::worker_loop(unsigned index) 195   185 thread_pool::worker_loop(unsigned index)
197   { 196   {
198   // Name format chosen to fit Linux's 15-char pthread limit: 197   // Name format chosen to fit Linux's 15-char pthread limit:
199   // "tpool-svc-" (10) + up to 4 digit index leaves "tpool-svc-9999". 198   // "tpool-svc-" (10) + up to 4 digit index leaves "tpool-svc-9999".
200   char name[16]; 199   char name[16];
HITCBC 201   185 std::snprintf(name, sizeof(name), "tpool-svc-%u", index); 200   185 std::snprintf(name, sizeof(name), "tpool-svc-%u", index);
HITCBC 202   185 capy::set_current_thread_name(name); 201   185 capy::set_current_thread_name(name);
203   202  
204   for (;;) 203   for (;;)
205   { 204   {
206   pool_work_item* w; 205   pool_work_item* w;
207   { 206   {
HITCBC 208   697 std::unique_lock<std::mutex> lock(mutex_); 207   697 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 209   697 cv_.wait( 208   697 cv_.wait(
HITCBC 210   934 lock, [this] { return shutdown_ || !work_queue_.empty(); }); 209   916 lock, [this] { return shutdown_ || !work_queue_.empty(); });
211   210  
HITCBC 212   697 w = work_queue_.pop(); 211   697 w = work_queue_.pop();
HITCBC 213   697 if (!w) 212   697 if (!w)
214   { 213   {
HITCBC 215   185 if (shutdown_) 214   185 if (shutdown_)
HITCBC 216   370 return; 215   370 return;
MISUBC 217   continue; 216   continue;
218   } 217   }
HITCBC 219   697 } 218   697 }
HITCBC 220   512 w->func_(w); 219   512 w->func_(w);
HITCBC 221   512 } 220   512 }
222   } 221   }
223   222  
224   // Called with mutex_ held, so the workers are started once however 223   // Called with mutex_ held, so the workers are started once however
225   // many threads race the first post. 224   // many threads race the first post.
226   inline std::error_code 225   inline std::error_code
HITCBC 227   516 thread_pool::start_workers() noexcept 226   516 thread_pool::start_workers() noexcept
228   { 227   {
HITCBC 229   516 if (!threads_.empty()) 228   516 if (!threads_.empty())
HITCBC 230   330 return {}; 229   330 return {};
HITCBC 231   186 std::error_code ec; 230   186 std::error_code ec;
232   try 231   try
233   { 232   {
HITCBC 234   186 threads_.reserve(num_threads_); 233   186 threads_.reserve(num_threads_);
HITCBC 235   370 for (unsigned i = 0; i < num_threads_; ++i) 234   370 for (unsigned i = 0; i < num_threads_; ++i)
HITCBC 236   373 threads_.emplace_back([this, i] { worker_loop(i + 1); }); 235   373 threads_.emplace_back([this, i] { worker_loop(i + 1); });
237   } 236   }
HITCBC 238   4 catch (std::system_error const& e) 237   4 catch (std::system_error const& e)
239   { 238   {
240   // The refusal is carried out, not swallowed: a thread the 239   // The refusal is carried out, not swallowed: a thread the
241   // system will not give is a real error and the operation that 240   // system will not give is a real error and the operation that
242   // asked for it says so, rather than reporting the cancellation 241   // asked for it says so, rather than reporting the cancellation
243   // that belongs to a stop token. 242   // that belongs to a stop token.
HITCBC 244   2 ec = e.code(); 243   2 ec = e.code();
HITCBC 245   2 } 244   2 }
HITCBC 246   2 catch (...) 245   2 catch (...)
247   { 246   {
HITCBC 248   2 ec = std::make_error_code(std::errc::resource_unavailable_try_again); 247   2 ec = std::make_error_code(std::errc::resource_unavailable_try_again);
HITCBC 249   2 } 248   2 }
250   // A pool short of workers still runs everything posted to it, only 249   // A pool short of workers still runs everything posted to it, only
251   // less of it at once, so a partial start is a start. What it does 250   // less of it at once, so a partial start is a start. What it does
252   // not do is come back for the rest: the size is a tuning knob, and 251   // not do is come back for the rest: the size is a tuning knob, and
253   // topping it up would put a thread creation on the initiator's 252   // topping it up would put a thread creation on the initiator's
254   // path for every operation after a refusal. 253   // path for every operation after a refusal.
HITCBC 255   186 if (!threads_.empty()) 254   186 if (!threads_.empty())
HITCBC 256   182 return {}; 255   182 return {};
HITCBC 257   4 return ec; 256   4 return ec;
258   } 257   }
259   258  
260   inline std::error_code 259   inline std::error_code
HITCBC 261   527 thread_pool::post(pool_work_item* w) noexcept 260   527 thread_pool::post(pool_work_item* w) noexcept
262   { 261   {
263   { 262   {
HITCBC 264   527 std::lock_guard<std::mutex> lock(mutex_); 263   527 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 265   527 if (shutdown_) 264   527 if (shutdown_)
HITCBC 266   11 return capy::error::canceled; 265   11 return capy::error::canceled;
267   // The system can refuse a thread, and an initiator has no way 266   // The system can refuse a thread, and an initiator has no way
268   // to throw; a refused post is the failure the callers already 267   // to throw; a refused post is the failure the callers already
269   // report through the operation they were starting. 268   // report through the operation they were starting.
HITCBC 270   516 if (auto ec = start_workers()) 269   516 if (auto ec = start_workers())
HITCBC 271   4 return ec; 270   4 return ec;
HITCBC 272   512 work_queue_.push(w); 271   512 work_queue_.push(w);
HITCBC 273   527 } 272   527 }
HITCBC 274   512 cv_.notify_one(); 273   512 cv_.notify_one();
HITCBC 275   512 return {}; 274   512 return {};
276   } 275   }
277   276  
278   inline void 277   inline void
HITCBC 279   2118 thread_pool::shutdown() 278   2118 thread_pool::shutdown()
280   { 279   {
281   { 280   {
HITCBC 282   2118 std::lock_guard<std::mutex> lock(mutex_); 281   2118 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 283   2118 shutdown_ = true; 282   2118 shutdown_ = true;
HITCBC 284   2118 } 283   2118 }
HITCBC 285   2118 cv_.notify_all(); 284   2118 cv_.notify_all();
286   285  
287   // Unlocked, though a post may add to threads_: the flag above is 286   // Unlocked, though a post may add to threads_: the flag above is
288   // published under the same mutex, so a post that has not taken it 287   // published under the same mutex, so a post that has not taken it
289   // yet will find it set and start nothing, and one already inside 288   // yet will find it set and start nothing, and one already inside
290   // released the mutex before this thread acquired it. 289   // released the mutex before this thread acquired it.
HITCBC 291   2303 for (auto& t : threads_) 290   2303 for (auto& t : threads_)
292   { 291   {
HITCBC 293   185 if (t.joinable()) 292   185 if (t.joinable())
HITCBC 294   185 t.join(); 293   185 t.join();
295   } 294   }
HITCBC 296   2118 threads_.clear(); 295   2118 threads_.clear();
297   296  
298   { 297   {
HITCBC 299   2118 std::lock_guard<std::mutex> lock(mutex_); 298   2118 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 300   2118 while (work_queue_.pop()) 299   2118 while (work_queue_.pop())
301   ; 300   ;
HITCBC 302   2118 } 301   2118 }
HITCBC 303   2118 } 302   2118 }
304   303  
305   /** A reference to the context's shared thread pool, bound on first use. 304   /** A reference to the context's shared thread pool, bound on first use.
306   305  
307   Services that hand blocking work to the pool hold one of these 306   Services that hand blocking work to the pool hold one of these
308   instead of a reference bound at construction. They are constructed 307   instead of a reference bound at construction. They are constructed
309   from the scheduler's constructor, where the pool they created would 308   from the scheduler's constructor, where the pool they created would
310   be older than the scheduler and would join too late; binding on 309   be older than the scheduler and would join too late; binding on
311   first use puts the pool after it instead. 310   first use puts the pool after it instead.
312   311  
313   The owning `io_context` creates the pool service during 312   The owning `io_context` creates the pool service during
314   construction, so by the time any operation can run the binding only 313   construction, so by the time any operation can run the binding only
315   ever finds it. That is what keeps `get()` from constructing 314   ever finds it. That is what keeps `get()` from constructing
316   anything on an initiator's thread, and so from throwing where an 315   anything on an initiator's thread, and so from throwing where an
317   initiator may not: the throwing spelling exists for a scheduler 316   initiator may not: the throwing spelling exists for a scheduler
318   driven without an `io_context`. What the service defers is its 317   driven without an `io_context`. What the service defers is its
319   workers, and those are started by `post()`, which reports a refusal 318   workers, and those are started by `post()`, which reports a refusal
320   rather than throwing it. 319   rather than throwing it.
321   320  
322   @par Thread Safety 321   @par Thread Safety
323   Distinct objects: Safe. 322   Distinct objects: Safe.
324   Shared objects: Safe. 323   Shared objects: Safe.
325   324  
326   @see thread_pool 325   @see thread_pool
327   */ 326   */
328   class thread_pool_ref 327   class thread_pool_ref
329   { 328   {
330   capy::execution_context& ctx_; 329   capy::execution_context& ctx_;
331   std::atomic<thread_pool*> pool_{nullptr}; 330   std::atomic<thread_pool*> pool_{nullptr};
332   331  
333   public: 332   public:
334   /** Construct a reference into the given context. 333   /** Construct a reference into the given context.
335   334  
336   @param ctx The context whose pool is used. 335   @param ctx The context whose pool is used.
337   */ 336   */
HITCBC 338 - 6318 explicit thread_pool_ref(capy::execution_context& ctx) noexcept 337 + 6318 explicit thread_pool_ref(capy::execution_context& ctx) noexcept : ctx_(ctx)
DCB 339 - 6318 : ctx_(ctx)  
340   { 338   {
HITCBC 341   6318 } 339   6318 }
342   340  
343   thread_pool_ref(thread_pool_ref const&) = delete; 341   thread_pool_ref(thread_pool_ref const&) = delete;
344   thread_pool_ref& operator=(thread_pool_ref const&) = delete; 342   thread_pool_ref& operator=(thread_pool_ref const&) = delete;
345   343  
346   /** Return the pool, creating it if this is the first use. 344   /** Return the pool, creating it if this is the first use.
347   345  
348   @par Preconditions 346   @par Preconditions
349   For the throwing clauses below to be unreachable, the owning 347   For the throwing clauses below to be unreachable, the owning
350   context must already hold the pool service. Every `io_context` 348   context must already hold the pool service. Every `io_context`
351   constructor installs it — what waits for a first post is the 349   constructor installs it — what waits for a first post is the
352   service's workers, not the service — so the creating branch is 350   service's workers, not the service — so the creating branch is
353   reached only by a scheduler driven without one. 351   reached only by a scheduler driven without one.
354   352  
355   @par Exception Safety 353   @par Exception Safety
356   Strong guarantee. 354   Strong guarantee.
357   355  
358   @throws std::bad_alloc If the service cannot be allocated. 356   @throws std::bad_alloc If the service cannot be allocated.
359   357  
360   @throws std::logic_error If the pool is asked for zero threads. 358   @throws std::logic_error If the pool is asked for zero threads.
361   359  
362   @return The context's shared thread pool. 360   @return The context's shared thread pool.
363   */ 361   */
HITCBC 364   503 thread_pool& get() 362   503 thread_pool& get()
365   { 363   {
HITCBC 366   503 auto* p = pool_.load(std::memory_order_acquire); 364   503 auto* p = pool_.load(std::memory_order_acquire);
HITCBC 367   503 if (!p) 365   503 if (!p)
368   { 366   {
HITCBC 369   186 p = &ctx_.use_service<thread_pool>(); 367   186 p = &ctx_.use_service<thread_pool>();
HITCBC 370   186 pool_.store(p, std::memory_order_release); 368   186 pool_.store(p, std::memory_order_release);
371   } 369   }
HITCBC 372   503 return *p; 370   503 return *p;
373   } 371   }
374   }; 372   };
375   373  
376   } // namespace boost::corosio::detail 374   } // namespace boost::corosio::detail
377   375  
378   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP 376   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP