100.00% Lines (79/79) 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   /** Teardown for an item the pool shuts down before running. 62   /** Teardown for an item the pool shuts down before running.
63   63  
64   An operation that owns a suspended coroutine sets this, so the 64   An operation that owns a suspended coroutine sets this, so the
65   coroutine is abandoned at shutdown instead of the pool making 65   coroutine is abandoned at shutdown instead of the pool making
66   the blocking call nobody is left to wait for. When null, the 66   the blocking call nobody is left to wait for. When null, the
67   item is run by `func_` instead. 67   item is run by `func_` instead.
68   */ 68   */
69   func_type destroy_ = nullptr; 69   func_type destroy_ = nullptr;
70   }; 70   };
71   71  
72   /** Shared thread pool for dispatching blocking operations. 72   /** Shared thread pool for dispatching blocking operations.
73   73  
74   Provides a fixed pool of reusable worker threads for operations 74   Provides a fixed pool of reusable worker threads for operations
75   that cannot be integrated with async I/O (e.g. blocking DNS 75   that cannot be integrated with async I/O (e.g. blocking DNS
76   calls). Registered as an `execution_context::service` so it 76   calls). Registered as an `execution_context::service` so it
77   is a singleton per io_context. 77   is a singleton per io_context.
78   78  
79   The service is created with its context, but the workers start on 79   The service is created with its context, but the workers start on
80   the first `post()`: a context that never opens a file and never 80   the first `post()`: a context that never opens a file and never
81   resolves a name never pays for a thread. The default thread count 81   resolves a name never pays for a thread. The default thread count
82   is 1. 82   is 1.
83   83  
84   @par Thread Safety 84   @par Thread Safety
85   All public member functions are thread-safe. 85   All public member functions are thread-safe.
86   86  
87   @par Shutdown 87   @par Shutdown
88   Sets a shutdown flag, notifies all threads, and joins them. 88   Sets a shutdown flag, notifies all threads, and joins them.
89   In-flight blocking calls complete naturally before the thread 89   In-flight blocking calls complete naturally before the thread
90   exits. Items still queued are not run: each is handed to its 90   exits. Items still queued are not run: each is handed to its
91   `destroy_`, or run by `func_` on the shutting-down thread when it 91   `destroy_`, or run by `func_` on the shutting-down thread when it
92   has none. 92   has none.
93   93  
94   @note Create this service after the scheduler its work items post 94   @note Create this service after the scheduler its work items post
95   completions to. Services shut down newest first, so a pool created 95   completions to. Services shut down newest first, so a pool created
96   earlier joins its workers only after the scheduler has drained its 96   earlier joins its workers only after the scheduler has drained its
97   completion queue, and the completion the last worker posts is then 97   completion queue, and the completion the last worker posts is then
98   destroyed only when the scheduler is deleted. 98   destroyed only when the scheduler is deleted.
99   99  
100   @note The type is symbol-visible because services are keyed by type 100   @note The type is symbol-visible because services are keyed by type
101   identity: with RTTI, hidden behind a shared library boundary, a 101   identity: with RTTI, hidden behind a shared library boundary, a
102   module that asks for the pool would look up, and create, one of its 102   module that asks for the pool would look up, and create, one of its
103   own (the no-RTTI key is a template static whose visibility follows 103   own (the no-RTTI key is a template static whose visibility follows
104   the template it is instantiated from). 104   the template it is instantiated from).
105   */ 105   */
106   class BOOST_COROSIO_SYMBOL_VISIBLE thread_pool final 106   class BOOST_COROSIO_SYMBOL_VISIBLE thread_pool final
107   : public capy::execution_context::service 107   : public capy::execution_context::service
108   { 108   {
109   std::mutex mutex_; 109   std::mutex mutex_;
110   std::condition_variable cv_; 110   std::condition_variable cv_;
111   intrusive_queue<pool_work_item> work_queue_; 111   intrusive_queue<pool_work_item> work_queue_;
112   std::vector<std::thread> threads_; 112   std::vector<std::thread> threads_;
113   unsigned num_threads_; 113   unsigned num_threads_;
114   bool shutdown_ = false; 114   bool shutdown_ = false;
115   115  
116   void worker_loop(unsigned index); 116   void worker_loop(unsigned index);
117   std::error_code start_workers() noexcept; 117   std::error_code start_workers() noexcept;
118   118  
119   public: 119   public:
120   using key_type = thread_pool; 120   using key_type = thread_pool;
121   121  
122   /** Construct the thread pool service. 122   /** Construct the thread pool service.
123   123  
124   Records the worker count. The workers themselves start on the 124   Records the worker count. The workers themselves start on the
125   first `post()`. 125   first `post()`.
126   126  
127   @par Exception Safety 127   @par Exception Safety
128   Strong guarantee. 128   Strong guarantee.
129   129  
130   @param ctx Reference to the owning execution_context. 130   @param ctx Reference to the owning execution_context.
131   @param num_threads Number of worker threads. Must be 131   @param num_threads Number of worker threads. Must be
132   at least 1. 132   at least 1.
133   133  
134   @throws std::logic_error If `num_threads` is 0. 134   @throws std::logic_error If `num_threads` is 0.
135   */ 135   */
HITCBC 136   4430 explicit thread_pool( 136   4419 explicit thread_pool(
137   [[maybe_unused]] capy::execution_context& ctx, unsigned num_threads = 1) 137   [[maybe_unused]] capy::execution_context& ctx, unsigned num_threads = 1)
HITCBC 138   4430 : num_threads_(num_threads) 138   4419 : num_threads_(num_threads)
139   { 139   {
HITCBC 140   4430 if (!num_threads) 140   4419 if (!num_threads)
HITCBC 141   1 throw std::logic_error("thread_pool requires at least 1 thread"); 141   1 throw std::logic_error("thread_pool requires at least 1 thread");
HITCBC 142   4432 } 142   4421 }
143   143  
144 - /** Destroy the pool, joining any worker `shutdown()` did not. 144 + /** Destroy the pool, joining any worker `shutdown()` never reached.
145   145  
146 - A context shuts down the pool it owns before destroying it, or 146 + The context's shutdown walk is the normal path; this only
147 - at creation if the context's shutdown already began. A pool 147 + catches a pool created after that walk, whose `shutdown()` is
148 - constructed directly gets no such call, so this shuts it down 148 + therefore never called and whose joinable threads would
149 - rather than leave a joinable worker to terminate the process. 149 + otherwise terminate the process. A pool that was never posted
  150 + to holds no thread and needs neither.
150   */ 151   */
HITCBC 151   8855 ~thread_pool() override 152   8833 ~thread_pool() override
HITCBC 152   4429 { 153   4418 {
HITCBC 153   4429 if (!threads_.empty()) 154   4418 if (!threads_.empty())
HITCBC 154   1 shutdown(); 155   1 shutdown();
HITCBC 155   8855 } 156   8833 }
156   157  
157   thread_pool(thread_pool const&) = delete; 158   thread_pool(thread_pool const&) = delete;
158   thread_pool& operator=(thread_pool const&) = delete; 159   thread_pool& operator=(thread_pool const&) = delete;
159   160  
160   /** Enqueue a work item for execution on the thread pool. 161   /** Enqueue a work item for execution on the thread pool.
161   162  
162   The first item posted starts the workers. Zero-allocation: 163   The first item posted starts the workers. Zero-allocation:
163   the caller owns the work item's storage. 164   the caller owns the work item's storage.
164   165  
165   A refusal answers with the code the caller reports for the 166   A refusal answers with the code the caller reports for the
166   operation it was starting, so that a system that will not give 167   operation it was starting, so that a system that will not give
167   the pool a thread is not mistaken for a cancellation. 168   the pool a thread is not mistaken for a cancellation.
168   169  
169   @par Thread Safety 170   @par Thread Safety
170   Safe. Racing first posts start the workers once. 171   Safe. Racing first posts start the workers once.
171   172  
172   @param w The work item to execute. Must remain valid until 173   @param w The work item to execute. Must remain valid until
173   its `func_` or `destroy_` has been called. 174   its `func_` or `destroy_` has been called.
174   175  
175   @return An empty code if the item was enqueued; 176   @return An empty code if the item was enqueued;
176   `capy::error::canceled` if the pool has already shut 177   `capy::error::canceled` if the pool has already shut
177   down; otherwise the code of the thread the system 178   down; otherwise the code of the thread the system
178   refused, which left the pool with no worker at all. 179   refused, which left the pool with no worker at all.
179   */ 180   */
180   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept; 181   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept;
181   182  
182   /** Return the number of workers the pool has started. 183   /** Return the number of workers the pool has started.
183   184  
184   Zero until the first `post()`, and zero again once 185   Zero until the first `post()`, and zero again once
185   `shutdown()` has joined them. 186   `shutdown()` has joined them.
186   187  
187   @par Thread Safety 188   @par Thread Safety
188   Safe. 189   Safe.
189   */ 190   */
HITCBC 190   6 unsigned worker_count() noexcept 191   6 unsigned worker_count() noexcept
191   { 192   {
HITCBC 192   6 std::lock_guard<std::mutex> lock(mutex_); 193   6 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 193   6 return static_cast<unsigned>(threads_.size()); 194   6 return static_cast<unsigned>(threads_.size());
HITCBC 194   6 } 195   6 }
195   196  
196   /** Shut down the thread pool. 197   /** Shut down the thread pool.
197   198  
198   Signals all threads to exit once their current item is done, 199   Signals all threads to exit once their current item is done,
199   joins them, then disposes of every item still queued without 200   joins them, then disposes of every item still queued without
200   running its blocking work where the item says how. 201   running its blocking work where the item says how.
201   */ 202   */
202   void shutdown() override; 203   void shutdown() override;
203   }; 204   };
204   205  
205   inline void 206   inline void
HITCBC 206   735 thread_pool::worker_loop(unsigned index) 207   733 thread_pool::worker_loop(unsigned index)
207   { 208   {
208   // Name format chosen to fit Linux's 15-char pthread limit: 209   // Name format chosen to fit Linux's 15-char pthread limit:
209   // "tpool-svc-" (10) + up to 4 digit index leaves "tpool-svc-9999". 210   // "tpool-svc-" (10) + up to 4 digit index leaves "tpool-svc-9999".
210   char name[16]; 211   char name[16];
HITCBC 211   735 std::snprintf(name, sizeof(name), "tpool-svc-%u", index); 212   733 std::snprintf(name, sizeof(name), "tpool-svc-%u", index);
HITCBC 212   735 capy::set_current_thread_name(name); 213   733 capy::set_current_thread_name(name);
213   214  
214   for (;;) 215   for (;;)
215   { 216   {
216   pool_work_item* w; 217   pool_work_item* w;
217   { 218   {
HITCBC 218   1967 std::unique_lock<std::mutex> lock(mutex_); 219   1970 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 219   1967 cv_.wait( 220   1970 cv_.wait(
HITCBC 220   2926 lock, [this] { return shutdown_ || !work_queue_.empty(); }); 221   2936 lock, [this] { return shutdown_ || !work_queue_.empty(); });
221   222  
222   // What is still queued is left to shutdown(), which does not 223   // What is still queued is left to shutdown(), which does not
223   // make blocking calls whose results nobody will read. 224   // make blocking calls whose results nobody will read.
HITCBC 224   1967 if (shutdown_) 225   1970 if (shutdown_)
HITCBC 225   1470 return; 226   1466 return;
HITCBC 226   1232 w = work_queue_.pop(); 227   1237 w = work_queue_.pop();
HITCBC 227   1967 } 228   1970 }
HITCBC 228   1232 w->func_(w); 229   1237 w->func_(w);
HITCBC 229   1232 } 230   1237 }
230   } 231   }
231   232  
232   // Called with mutex_ held, so the workers are started once however 233   // Called with mutex_ held, so the workers are started once however
233   // many threads race the first post. 234   // many threads race the first post.
234   inline std::error_code 235   inline std::error_code
HITCBC 235   1286 thread_pool::start_workers() noexcept 236   2199 thread_pool::start_workers() noexcept
236   { 237   {
HITCBC 237   1286 if (!threads_.empty()) 238   2199 if (!threads_.empty())
HITCBC 238   550 return {}; 239   1465 return {};
HITCBC 239   736 std::error_code ec; 240   734 std::error_code ec;
240   try 241   try
241   { 242   {
HITCBC 242   736 threads_.reserve(num_threads_); 243   734 threads_.reserve(num_threads_);
HITCBC 243   1470 for (unsigned i = 0; i < num_threads_; ++i) 244   1466 for (unsigned i = 0; i < num_threads_; ++i)
HITCBC 244   1473 threads_.emplace_back([this, i] { worker_loop(i + 1); }); 245   1469 threads_.emplace_back([this, i] { worker_loop(i + 1); });
245   } 246   }
HITCBC 246   4 catch (std::system_error const& e) 247   4 catch (std::system_error const& e)
247   { 248   {
248   // The refusal is carried out, not swallowed: a thread the 249   // The refusal is carried out, not swallowed: a thread the
249   // system will not give is a real error and the operation that 250   // system will not give is a real error and the operation that
250   // asked for it says so, rather than reporting the cancellation 251   // asked for it says so, rather than reporting the cancellation
251   // that belongs to a stop token. 252   // that belongs to a stop token.
HITCBC 252   2 ec = e.code(); 253   2 ec = e.code();
HITCBC 253   2 } 254   2 }
HITCBC 254   2 catch (...) 255   2 catch (...)
255   { 256   {
HITCBC 256   2 ec = std::make_error_code(std::errc::resource_unavailable_try_again); 257   2 ec = std::make_error_code(std::errc::resource_unavailable_try_again);
HITCBC 257   2 } 258   2 }
258   // A pool short of workers still runs everything posted to it, only 259   // A pool short of workers still runs everything posted to it, only
259   // less of it at once, so a partial start is a start. What it does 260   // less of it at once, so a partial start is a start. What it does
260   // not do is come back for the rest: the size is a tuning knob, and 261   // not do is come back for the rest: the size is a tuning knob, and
261   // topping it up would put a thread creation on the initiator's 262   // topping it up would put a thread creation on the initiator's
262   // path for every operation after a refusal. 263   // path for every operation after a refusal.
HITCBC 263   736 if (!threads_.empty()) 264   734 if (!threads_.empty())
HITCBC 264   732 return {}; 265   730 return {};
HITCBC 265   4 return ec; 266   4 return ec;
266   } 267   }
267   268  
268   inline std::error_code 269   inline std::error_code
HITCBC 269   1308 thread_pool::post(pool_work_item* w) noexcept 270   2219 thread_pool::post(pool_work_item* w) noexcept
270   { 271   {
271   { 272   {
HITCBC 272   1308 std::lock_guard<std::mutex> lock(mutex_); 273   2219 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 273   1308 if (shutdown_) 274   2219 if (shutdown_)
HITCBC 274   22 return capy::error::canceled; 275   20 return capy::error::canceled;
275   // The system can refuse a thread, and an initiator has no way 276   // The system can refuse a thread, and an initiator has no way
276   // to throw; a refused post is the failure the callers already 277   // to throw; a refused post is the failure the callers already
277   // report through the operation they were starting. 278   // report through the operation they were starting.
HITCBC 278   1286 if (auto ec = start_workers()) 279   2199 if (auto ec = start_workers())
HITCBC 279   4 return ec; 280   4 return ec;
HITCBC 280   1282 work_queue_.push(w); 281   2195 work_queue_.push(w);
HITCBC 281   1308 } 282   2219 }
HITCBC 282   1282 cv_.notify_one(); 283   2195 cv_.notify_one();
HITCBC 283   1282 return {}; 284   2195 return {};
284   } 285   }
285   286  
286   inline void 287   inline void
HITCBC 287   4447 thread_pool::shutdown() 288   4436 thread_pool::shutdown()
288   { 289   {
289   { 290   {
HITCBC 290   4447 std::lock_guard<std::mutex> lock(mutex_); 291   4436 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 291   4447 shutdown_ = true; 292   4436 shutdown_ = true;
HITCBC 292   4447 } 293   4436 }
HITCBC 293   4447 cv_.notify_all(); 294   4436 cv_.notify_all();
294   295  
295   // Unlocked, though a post may add to threads_: the flag above is 296   // Unlocked, though a post may add to threads_: the flag above is
296   // published under the same mutex, so a post that has not taken it 297   // published under the same mutex, so a post that has not taken it
297   // yet will find it set and start nothing, and one already inside 298   // yet will find it set and start nothing, and one already inside
298   // released the mutex before this thread acquired it. 299   // released the mutex before this thread acquired it.
HITCBC 299   5182 for (auto& t : threads_) 300   5169 for (auto& t : threads_)
300   { 301   {
HITCBC 301   735 if (t.joinable()) 302   733 if (t.joinable())
HITCBC 302   735 t.join(); 303   733 t.join();
303   } 304   }
HITCBC 304   4447 threads_.clear(); 305   4436 threads_.clear();
305   306  
HITCBC 306   4447 intrusive_queue<pool_work_item> leftover; 307   4436 intrusive_queue<pool_work_item> leftover;
307   { 308   {
HITCBC 308   4447 std::lock_guard<std::mutex> lock(mutex_); 309   4436 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 309   4447 leftover.splice(work_queue_); 310   4436 leftover.splice(work_queue_);
HITCBC 310   4447 } 311   4436 }
311   // Unlocked: an abandoned coroutine's unwind can reach post() 312   // Unlocked: an abandoned coroutine's unwind can reach post()
312   // through the objects its frame owned. 313   // through the objects its frame owned.
HITCBC 313   4497 while (auto* w = leftover.pop()) 314   5394 while (auto* w = leftover.pop())
314   { 315   {
HITCBC 315   50 if (w->destroy_) 316   958 if (w->destroy_)
HITCBC 316   28 w->destroy_(w); 317   939 w->destroy_(w);
317   else 318   else
HITCBC 318   22 w->func_(w); 319   19 w->func_(w);
HITCBC 319   50 } 320   958 }
HITCBC 320   4447 } 321   4436 }
321   322  
322   /** A reference to the context's shared thread pool, bound on first use. 323   /** A reference to the context's shared thread pool, bound on first use.
323   324  
324   Services that hand blocking work to the pool hold one of these 325   Services that hand blocking work to the pool hold one of these
325   instead of a reference bound at construction. A service made before 326   instead of a reference bound at construction. A service made before
326   the pool, as one made by the scheduler's constructor would be, would 327   the pool, as one made by the scheduler's constructor would be, would
327   otherwise create the pool itself, older than the scheduler and so 328   otherwise create the pool itself, older than the scheduler and so
328   joined too late; binding on first use puts the pool after it. 329   joined too late; binding on first use puts the pool after it.
329   330  
330   The owning `io_context` creates the pool service during 331   The owning `io_context` creates the pool service during
331   construction, so by the time any operation can run the binding only 332   construction, so by the time any operation can run the binding only
332   ever finds it. That is what keeps `get()` from constructing 333   ever finds it. That is what keeps `get()` from constructing
333   anything on an initiator's thread, and so from throwing where an 334   anything on an initiator's thread, and so from throwing where an
334   initiator may not: the throwing spelling exists for a scheduler 335   initiator may not: the throwing spelling exists for a scheduler
335   driven without an `io_context`. What the service defers is its 336   driven without an `io_context`. What the service defers is its
336   workers, and those are started by `post()`, which reports a refusal 337   workers, and those are started by `post()`, which reports a refusal
337   rather than throwing it. 338   rather than throwing it.
338   339  
339   @par Thread Safety 340   @par Thread Safety
340   Distinct objects: Safe. 341   Distinct objects: Safe.
341   Shared objects: Safe. 342   Shared objects: Safe.
342   343  
343   @see thread_pool 344   @see thread_pool
344   */ 345   */
345   class thread_pool_ref 346   class thread_pool_ref
346   { 347   {
347   capy::execution_context& ctx_; 348   capy::execution_context& ctx_;
348   std::atomic<thread_pool*> pool_{nullptr}; 349   std::atomic<thread_pool*> pool_{nullptr};
349   350  
350   public: 351   public:
351   /** Construct a reference into the given context. 352   /** Construct a reference into the given context.
352   353  
353   @param ctx The context whose pool is used. 354   @param ctx The context whose pool is used.
354   */ 355   */
HITCBC 355   907 explicit thread_pool_ref(capy::execution_context& ctx) noexcept : ctx_(ctx) 356   905 explicit thread_pool_ref(capy::execution_context& ctx) noexcept : ctx_(ctx)
356   { 357   {
HITCBC 357   907 } 358   905 }
358   359  
359   thread_pool_ref(thread_pool_ref const&) = delete; 360   thread_pool_ref(thread_pool_ref const&) = delete;
360   thread_pool_ref& operator=(thread_pool_ref const&) = delete; 361   thread_pool_ref& operator=(thread_pool_ref const&) = delete;
361   362  
362   /** Return the pool, creating it if this is the first use. 363   /** Return the pool, creating it if this is the first use.
363   364  
364   @pre For the throwing clauses below to be unreachable, the owning 365   @pre For the throwing clauses below to be unreachable, the owning
365   context must already hold the pool service. Every `io_context` 366   context must already hold the pool service. Every `io_context`
366   constructor installs it — what waits for a first post is the 367   constructor installs it — what waits for a first post is the
367   service's workers, not the service — so the creating branch is 368   service's workers, not the service — so the creating branch is
368   reached only by a scheduler driven without one. 369   reached only by a scheduler driven without one.
369   370  
370   @par Exception Safety 371   @par Exception Safety
371   Strong guarantee. 372   Strong guarantee.
372   373  
373   @throws std::bad_alloc If the service cannot be allocated. 374   @throws std::bad_alloc If the service cannot be allocated.
374   375  
375   @throws std::logic_error If the pool is asked for zero threads. 376   @throws std::logic_error If the pool is asked for zero threads.
376   377  
377   @return The context's shared thread pool. 378   @return The context's shared thread pool.
378   */ 379   */
HITCBC 379   1235 thread_pool& get() 380   1237 thread_pool& get()
380   { 381   {
HITCBC 381   1235 auto* p = pool_.load(std::memory_order_acquire); 382   1237 auto* p = pool_.load(std::memory_order_acquire);
HITCBC 382   1235 if (!p) 383   1237 if (!p)
383   { 384   {
HITCBC 384   735 p = &ctx_.use_service<thread_pool>(); 385   733 p = &ctx_.use_service<thread_pool>();
HITCBC 385   735 pool_.store(p, std::memory_order_release); 386   733 pool_.store(p, std::memory_order_release);
386   } 387   }
HITCBC 387   1235 return *p; 388   1237 return *p;
388   } 389   }
389   }; 390   };
390   391  
391   } // namespace boost::corosio::detail 392   } // namespace boost::corosio::detail
392   393  
393   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP 394   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP