LCOV - code coverage report
Current view: top level - corosio/detail - thread_pool.hpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 100.0 % 79 79
Test Date: 2026-10-17 04:01:32 Functions: 100.0 % 12 12

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

Generated by: LCOV version 2.3