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