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   4417 explicit thread_pool( 136   4417 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   4417 : num_threads_(num_threads) 138   4417 : num_threads_(num_threads)
139   { 139   {
HITCBC 140   4417 if (!num_threads) 140   4417 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   4419 } 142   4419 }
143   143  
144   /** Destroy the pool, joining any worker `shutdown()` never reached. 144   /** Destroy the pool, joining any worker `shutdown()` never reached.
145   145  
146   The context's shutdown walk is the normal path; this only 146   The context's shutdown walk is the normal path; this only
147   catches a pool created after that walk, whose `shutdown()` is 147   catches a pool created after that walk, whose `shutdown()` is
148   therefore never called and whose joinable threads would 148   therefore never called and whose joinable threads would
149   otherwise terminate the process. A pool that was never posted 149   otherwise terminate the process. A pool that was never posted
150   to holds no thread and needs neither. 150   to holds no thread and needs neither.
151   */ 151   */
HITCBC 152   8829 ~thread_pool() override 152   8829 ~thread_pool() override
HITCBC 153   4416 { 153   4416 {
HITCBC 154   4416 if (!threads_.empty()) 154   4416 if (!threads_.empty())
HITCBC 155   1 shutdown(); 155   1 shutdown();
HITCBC 156   8829 } 156   8829 }
157   157  
158   thread_pool(thread_pool const&) = delete; 158   thread_pool(thread_pool const&) = delete;
159   thread_pool& operator=(thread_pool const&) = delete; 159   thread_pool& operator=(thread_pool const&) = delete;
160   160  
161   /** Enqueue a work item for execution on the thread pool. 161   /** Enqueue a work item for execution on the thread pool.
162   162  
163   The first item posted starts the workers. Zero-allocation: 163   The first item posted starts the workers. Zero-allocation:
164   the caller owns the work item's storage. 164   the caller owns the work item's storage.
165   165  
166   A refusal answers with the code the caller reports for the 166   A refusal answers with the code the caller reports for the
167   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
168   the pool a thread is not mistaken for a cancellation. 168   the pool a thread is not mistaken for a cancellation.
169   169  
170   @par Thread Safety 170   @par Thread Safety
171   Safe. Racing first posts start the workers once. 171   Safe. Racing first posts start the workers once.
172   172  
173   @param w The work item to execute. Must remain valid until 173   @param w The work item to execute. Must remain valid until
174   its `func_` or `destroy_` has been called. 174   its `func_` or `destroy_` has been called.
175   175  
176   @return An empty code if the item was enqueued; 176   @return An empty code if the item was enqueued;
177   `capy::error::canceled` if the pool has already shut 177   `capy::error::canceled` if the pool has already shut
178   down; otherwise the code of the thread the system 178   down; otherwise the code of the thread the system
179   refused, which left the pool with no worker at all. 179   refused, which left the pool with no worker at all.
180   */ 180   */
181   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept; 181   [[nodiscard]] std::error_code post(pool_work_item* w) noexcept;
182   182  
183   /** Return the number of workers the pool has started. 183   /** Return the number of workers the pool has started.
184   184  
185   Zero until the first `post()`, and zero again once 185   Zero until the first `post()`, and zero again once
186   `shutdown()` has joined them. 186   `shutdown()` has joined them.
187   187  
188   @par Thread Safety 188   @par Thread Safety
189   Safe. 189   Safe.
190   */ 190   */
HITCBC 191   6 unsigned worker_count() noexcept 191   6 unsigned worker_count() noexcept
192   { 192   {
HITCBC 193   6 std::lock_guard<std::mutex> lock(mutex_); 193   6 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 194   6 return static_cast<unsigned>(threads_.size()); 194   6 return static_cast<unsigned>(threads_.size());
HITCBC 195   6 } 195   6 }
196   196  
197   /** Shut down the thread pool. 197   /** Shut down the thread pool.
198   198  
199   Signals all threads to exit once their current item is done, 199   Signals all threads to exit once their current item is done,
200   joins them, then disposes of every item still queued without 200   joins them, then disposes of every item still queued without
201   running its blocking work where the item says how. 201   running its blocking work where the item says how.
202   */ 202   */
203   void shutdown() override; 203   void shutdown() override;
204   }; 204   };
205   205  
206   inline void 206   inline void
HITCBC 207   733 thread_pool::worker_loop(unsigned index) 207   733 thread_pool::worker_loop(unsigned index)
208   { 208   {
209   // Name format chosen to fit Linux's 15-char pthread limit: 209   // Name format chosen to fit Linux's 15-char pthread limit:
210   // "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".
211   char name[16]; 211   char name[16];
HITCBC 212   733 std::snprintf(name, sizeof(name), "tpool-svc-%u", index); 212   733 std::snprintf(name, sizeof(name), "tpool-svc-%u", index);
HITCBC 213   733 capy::set_current_thread_name(name); 213   733 capy::set_current_thread_name(name);
214   214  
215   for (;;) 215   for (;;)
216   { 216   {
217   pool_work_item* w; 217   pool_work_item* w;
218   { 218   {
HITCBC 219   1994 std::unique_lock<std::mutex> lock(mutex_); 219   1978 std::unique_lock<std::mutex> lock(mutex_);
HITCBC 220   1994 cv_.wait( 220   1978 cv_.wait(
HITCBC 221   2984 lock, [this] { return shutdown_ || !work_queue_.empty(); }); 221   2953 lock, [this] { return shutdown_ || !work_queue_.empty(); });
222   222  
223   // What is still queued is left to shutdown(), which does not 223   // What is still queued is left to shutdown(), which does not
224   // make blocking calls whose results nobody will read. 224   // make blocking calls whose results nobody will read.
HITCBC 225   1994 if (shutdown_) 225   1978 if (shutdown_)
HITCBC 226   1466 return; 226   1466 return;
HITCBC 227   1261 w = work_queue_.pop(); 227   1245 w = work_queue_.pop();
HITCBC 228   1994 } 228   1978 }
HITCBC 229   1261 w->func_(w); 229   1245 w->func_(w);
HITCBC 230   1261 } 230   1245 }
231   } 231   }
232   232  
233   // Called with mutex_ held, so the workers are started once however 233   // Called with mutex_ held, so the workers are started once however
234   // many threads race the first post. 234   // many threads race the first post.
235   inline std::error_code 235   inline std::error_code
HITCBC 236   1309 thread_pool::start_workers() noexcept 236   4728 thread_pool::start_workers() noexcept
237   { 237   {
HITCBC 238   1309 if (!threads_.empty()) 238   4728 if (!threads_.empty())
HITCBC 239   575 return {}; 239   3994 return {};
HITCBC 240   734 std::error_code ec; 240   734 std::error_code ec;
241   try 241   try
242   { 242   {
HITCBC 243   734 threads_.reserve(num_threads_); 243   734 threads_.reserve(num_threads_);
HITCBC 244   1466 for (unsigned i = 0; i < num_threads_; ++i) 244   1466 for (unsigned i = 0; i < num_threads_; ++i)
HITCBC 245   1469 threads_.emplace_back([this, i] { worker_loop(i + 1); }); 245   1469 threads_.emplace_back([this, i] { worker_loop(i + 1); });
246   } 246   }
HITCBC 247   4 catch (std::system_error const& e) 247   4 catch (std::system_error const& e)
248   { 248   {
249   // The refusal is carried out, not swallowed: a thread the 249   // The refusal is carried out, not swallowed: a thread the
250   // 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
251   // asked for it says so, rather than reporting the cancellation 251   // asked for it says so, rather than reporting the cancellation
252   // that belongs to a stop token. 252   // that belongs to a stop token.
HITCBC 253   2 ec = e.code(); 253   2 ec = e.code();
HITCBC 254   2 } 254   2 }
HITCBC 255   2 catch (...) 255   2 catch (...)
256   { 256   {
HITCBC 257   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 258   2 } 258   2 }
259   // 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
260   // 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
261   // 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
262   // 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
263   // path for every operation after a refusal. 263   // path for every operation after a refusal.
HITCBC 264   734 if (!threads_.empty()) 264   734 if (!threads_.empty())
HITCBC 265   730 return {}; 265   730 return {};
HITCBC 266   4 return ec; 266   4 return ec;
267   } 267   }
268   268  
269   inline std::error_code 269   inline std::error_code
HITCBC 270   1329 thread_pool::post(pool_work_item* w) noexcept 270   4748 thread_pool::post(pool_work_item* w) noexcept
271   { 271   {
272   { 272   {
HITCBC 273   1329 std::lock_guard<std::mutex> lock(mutex_); 273   4748 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 274   1329 if (shutdown_) 274   4748 if (shutdown_)
HITCBC 275   20 return capy::error::canceled; 275   20 return capy::error::canceled;
276   // 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
277   // to throw; a refused post is the failure the callers already 277   // to throw; a refused post is the failure the callers already
278   // report through the operation they were starting. 278   // report through the operation they were starting.
HITCBC 279   1309 if (auto ec = start_workers()) 279   4728 if (auto ec = start_workers())
HITCBC 280   4 return ec; 280   4 return ec;
HITCBC 281   1305 work_queue_.push(w); 281   4724 work_queue_.push(w);
HITCBC 282   1329 } 282   4748 }
HITCBC 283   1305 cv_.notify_one(); 283   4724 cv_.notify_one();
HITCBC 284   1305 return {}; 284   4724 return {};
285   } 285   }
286   286  
287   inline void 287   inline void
HITCBC 288   4434 thread_pool::shutdown() 288   4434 thread_pool::shutdown()
289   { 289   {
290   { 290   {
HITCBC 291   4434 std::lock_guard<std::mutex> lock(mutex_); 291   4434 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 292   4434 shutdown_ = true; 292   4434 shutdown_ = true;
HITCBC 293   4434 } 293   4434 }
HITCBC 294   4434 cv_.notify_all(); 294   4434 cv_.notify_all();
295   295  
296   // 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
297   // 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
298   // 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
299   // released the mutex before this thread acquired it. 299   // released the mutex before this thread acquired it.
HITCBC 300   5167 for (auto& t : threads_) 300   5167 for (auto& t : threads_)
301   { 301   {
HITCBC 302   733 if (t.joinable()) 302   733 if (t.joinable())
HITCBC 303   733 t.join(); 303   733 t.join();
304   } 304   }
HITCBC 305   4434 threads_.clear(); 305   4434 threads_.clear();
306   306  
HITCBC 307   4434 intrusive_queue<pool_work_item> leftover; 307   4434 intrusive_queue<pool_work_item> leftover;
308   { 308   {
HITCBC 309   4434 std::lock_guard<std::mutex> lock(mutex_); 309   4434 std::lock_guard<std::mutex> lock(mutex_);
HITCBC 310   4434 leftover.splice(work_queue_); 310   4434 leftover.splice(work_queue_);
HITCBC 311   4434 } 311   4434 }
312   // Unlocked: an abandoned coroutine's unwind can reach post() 312   // Unlocked: an abandoned coroutine's unwind can reach post()
313   // through the objects its frame owned. 313   // through the objects its frame owned.
HITCBC 314   4478 while (auto* w = leftover.pop()) 314   7913 while (auto* w = leftover.pop())
315   { 315   {
HITCBC 316   44 if (w->destroy_) 316   3479 if (w->destroy_)
HITCBC 317   26 w->destroy_(w); 317   3457 w->destroy_(w);
318   else 318   else
HITCBC 319   18 w->func_(w); 319   22 w->func_(w);
HITCBC 320   44 } 320   3479 }
HITCBC 321   4434 } 321   4434 }
322   322  
323   /** 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.
324   324  
325   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
326   instead of a reference bound at construction. A service made before 326   instead of a reference bound at construction. A service made before
327   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
328   otherwise create the pool itself, older than the scheduler and so 328   otherwise create the pool itself, older than the scheduler and so
329   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.
330   330  
331   The owning `io_context` creates the pool service during 331   The owning `io_context` creates the pool service during
332   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
333   ever finds it. That is what keeps `get()` from constructing 333   ever finds it. That is what keeps `get()` from constructing
334   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
335   initiator may not: the throwing spelling exists for a scheduler 335   initiator may not: the throwing spelling exists for a scheduler
336   driven without an `io_context`. What the service defers is its 336   driven without an `io_context`. What the service defers is its
337   workers, and those are started by `post()`, which reports a refusal 337   workers, and those are started by `post()`, which reports a refusal
338   rather than throwing it. 338   rather than throwing it.
339   339  
340   @par Thread Safety 340   @par Thread Safety
341   Distinct objects: Safe. 341   Distinct objects: Safe.
342   Shared objects: Safe. 342   Shared objects: Safe.
343   343  
344   @see thread_pool 344   @see thread_pool
345   */ 345   */
346   class thread_pool_ref 346   class thread_pool_ref
347   { 347   {
348   capy::execution_context& ctx_; 348   capy::execution_context& ctx_;
349   std::atomic<thread_pool*> pool_{nullptr}; 349   std::atomic<thread_pool*> pool_{nullptr};
350   350  
351   public: 351   public:
352   /** Construct a reference into the given context. 352   /** Construct a reference into the given context.
353   353  
354   @param ctx The context whose pool is used. 354   @param ctx The context whose pool is used.
355   */ 355   */
HITCBC 356   905 explicit thread_pool_ref(capy::execution_context& ctx) noexcept : ctx_(ctx) 356   905 explicit thread_pool_ref(capy::execution_context& ctx) noexcept : ctx_(ctx)
357   { 357   {
HITCBC 358   905 } 358   905 }
359   359  
360   thread_pool_ref(thread_pool_ref const&) = delete; 360   thread_pool_ref(thread_pool_ref const&) = delete;
361   thread_pool_ref& operator=(thread_pool_ref const&) = delete; 361   thread_pool_ref& operator=(thread_pool_ref const&) = delete;
362   362  
363   /** Return the pool, creating it if this is the first use. 363   /** Return the pool, creating it if this is the first use.
364   364  
365   @pre For the throwing clauses below to be unreachable, the owning 365   @pre For the throwing clauses below to be unreachable, the owning
366   context must already hold the pool service. Every `io_context` 366   context must already hold the pool service. Every `io_context`
367   constructor installs it — what waits for a first post is the 367   constructor installs it — what waits for a first post is the
368   service's workers, not the service — so the creating branch is 368   service's workers, not the service — so the creating branch is
369   reached only by a scheduler driven without one. 369   reached only by a scheduler driven without one.
370   370  
371   @par Exception Safety 371   @par Exception Safety
372   Strong guarantee. 372   Strong guarantee.
373   373  
374   @throws std::bad_alloc If the service cannot be allocated. 374   @throws std::bad_alloc If the service cannot be allocated.
375   375  
376   @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.
377   377  
378   @return The context's shared thread pool. 378   @return The context's shared thread pool.
379   */ 379   */
HITCBC 380   1260 thread_pool& get() 380   1248 thread_pool& get()
381   { 381   {
HITCBC 382   1260 auto* p = pool_.load(std::memory_order_acquire); 382   1248 auto* p = pool_.load(std::memory_order_acquire);
HITCBC 383   1260 if (!p) 383   1248 if (!p)
384   { 384   {
HITCBC 385   733 p = &ctx_.use_service<thread_pool>(); 385   733 p = &ctx_.use_service<thread_pool>();
HITCBC 386   733 pool_.store(p, std::memory_order_release); 386   733 pool_.store(p, std::memory_order_release);
387   } 387   }
HITCBC 388   1260 return *p; 388   1248 return *p;
389   } 389   }
390   }; 390   };
391   391  
392   } // namespace boost::corosio::detail 392   } // namespace boost::corosio::detail
393   393  
394   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP 394   #endif // BOOST_COROSIO_DETAIL_THREAD_POOL_HPP