LCOV - code coverage report
Current view: top level - /jenkins/workspace/boost-root/libs/capy/src/ex - thread_pool.cpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 100.0 % 140 140
Test Date: 2026-07-23 16:13:07 Functions: 100.0 % 29 29

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
       3                 : // Copyright (c) 2026 Michael Vandeberg
       4                 : //
       5                 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
       6                 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
       7                 : //
       8                 : // Official repository: https://github.com/boostorg/capy
       9                 : //
      10                 : 
      11                 : #include <boost/capy/ex/thread_pool.hpp>
      12                 : #include <boost/capy/continuation.hpp>
      13                 : #include <boost/capy/detail/thread_local_ptr.hpp>
      14                 : #include <boost/capy/ex/frame_allocator.hpp>
      15                 : #include <boost/capy/test/thread_name.hpp>
      16                 : #include <algorithm>
      17                 : #include <atomic>
      18                 : #include <condition_variable>
      19                 : #include <cstdio>
      20                 : #include <mutex>
      21                 : #include <thread>
      22                 : #include <vector>
      23                 : 
      24                 : /*
      25                 :     Thread pool implementation using a shared work queue.
      26                 : 
      27                 :     Work items are continuations linked via their intrusive next pointer,
      28                 :     stored in a single queue protected by a mutex. No per-post heap
      29                 :     allocation: the continuation is owned by the caller and linked
      30                 :     directly. Worker threads wait on a condition_variable until work
      31                 :     is available or stop is requested.
      32                 : 
      33                 :     Threads are started lazily on first post() via std::call_once to avoid
      34                 :     spawning threads for pools that are constructed but never used. Each
      35                 :     thread is named with a configurable prefix plus index for debugger
      36                 :     visibility.
      37                 : 
      38                 :     Work tracking: on_work_started/on_work_finished maintain the atomic
      39                 :     outstanding_work_ counter. on_work_started is lock-free; the worker
      40                 :     that drives the count to zero takes mutex_ and re-reads the count
      41                 :     before deciding to stop, so the count and the stop decision stay
      42                 :     consistent even if work is started in between. join() blocks until
      43                 :     this counter reaches zero, then signals workers to stop and joins
      44                 :     threads.
      45                 : 
      46                 :     Two shutdown paths:
      47                 :     - join(): waits for outstanding work to drain, then stops workers.
      48                 :     - stop(): immediately signals workers to exit; queued work is abandoned.
      49                 :     - Destructor: stop() then join() (abandon + wait for threads).
      50                 : */
      51                 : 
      52                 : namespace boost {
      53                 : namespace capy {
      54                 : 
      55                 : //------------------------------------------------------------------------------
      56                 : 
      57                 : class thread_pool::impl
      58                 : {
      59                 :     // Identifies the pool owning the current worker thread, or
      60                 :     // nullptr if the calling thread is not a pool worker. Checked
      61                 :     // by dispatch() to decide between symmetric transfer (inline
      62                 :     // resume) and post.
      63                 :     static inline detail::thread_local_ptr<impl const> current_;
      64                 : 
      65                 :     // Intrusive queue of continuations: the next link is stored in
      66                 :     // continuation::reserved (typed continuation* round-tripped through
      67                 :     // void*). No per-post allocation: the continuation is owned by the caller.
      68                 :     continuation* head_ = nullptr;
      69                 :     continuation* tail_ = nullptr;
      70                 : 
      71 HIT       18077 :     void push(continuation* c) noexcept
      72                 :     {
      73           18077 :         c->reserved = nullptr;
      74           18077 :         if(tail_)
      75            5204 :             tail_->reserved = c;
      76                 :         else
      77           12873 :             head_ = c;
      78           18077 :         tail_ = c;
      79           18077 :     }
      80                 : 
      81           18412 :     continuation* pop() noexcept
      82                 :     {
      83           18412 :         if(!head_)
      84             335 :             return nullptr;
      85           18077 :         continuation* c = head_;
      86           18077 :         head_ = static_cast<continuation*>(head_->reserved);
      87           18077 :         if(!head_)
      88           12873 :             tail_ = nullptr;
      89           18077 :         return c;
      90                 :     }
      91                 : 
      92           31165 :     bool empty() const noexcept
      93                 :     {
      94           31165 :         return head_ == nullptr;
      95                 :     }
      96                 : 
      97                 :     std::mutex mutex_;
      98                 :     std::condition_variable work_cv_;
      99                 :     std::condition_variable done_cv_;
     100                 :     std::vector<std::thread> threads_;
     101                 :     std::atomic<std::size_t> outstanding_work_{0};
     102                 :     bool stop_{false};
     103                 :     bool joined_{false};
     104                 :     std::size_t num_threads_;
     105                 :     char thread_name_prefix_[13]{};  // 12 chars max + null terminator
     106                 :     std::once_flag start_flag_;
     107                 : 
     108                 : public:
     109             335 :     ~impl() = default;
     110                 : 
     111                 :     bool
     112             517 :     running_in_this_thread() const noexcept
     113                 :     {
     114             517 :         return current_.get() == this;
     115                 :     }
     116                 : 
     117                 :     // Destroy abandoned coroutine frames. Must be called
     118                 :     // before execution_context::shutdown()/destroy() so
     119                 :     // that suspended-frame destructors touching services
     120                 :     // (e.g. cancelling registrations) run while those
     121                 :     // services are still valid.
     122                 :     void
     123             335 :     drain_abandoned() noexcept
     124                 :     {
     125             500 :         while(auto* c = pop())
     126                 :         {
     127             165 :             auto h = c->h;
     128             165 :             if(h && h != std::noop_coroutine())
     129             114 :                 h.destroy();
     130             165 :         }
     131             335 :     }
     132                 : 
     133             335 :     impl(std::size_t num_threads, std::string_view thread_name_prefix)
     134             335 :         : num_threads_(num_threads)
     135                 :     {
     136             335 :         if(num_threads_ == 0)
     137               4 :             num_threads_ = std::max(
     138               2 :                 std::thread::hardware_concurrency(), 1u);
     139                 : 
     140                 :         // Truncate prefix to 12 chars, leaving room for up to 3-digit index.
     141             335 :         auto n = thread_name_prefix.copy(thread_name_prefix_, 12);
     142             335 :         thread_name_prefix_[n] = '\0';
     143             335 :     }
     144                 : 
     145                 :     void
     146           18077 :     post(continuation& c)
     147                 :     {
     148           18077 :         ensure_started();
     149                 :         {
     150           18077 :             std::lock_guard<std::mutex> lock(mutex_);
     151           18077 :             push(&c);
     152                 :             // Under the lock so the pool cannot drain, join, and
     153                 :             // destroy the condition variable mid-signal.
     154           18077 :             work_cv_.notify_one();
     155           18077 :         }
     156           18077 :     }
     157                 : 
     158                 :     void
     159             517 :     on_work_started() noexcept
     160                 :     {
     161             517 :         outstanding_work_.fetch_add(1, std::memory_order_acq_rel);
     162             517 :     }
     163                 : 
     164                 :     void
     165             517 :     on_work_finished() noexcept
     166                 :     {
     167             517 :         if(outstanding_work_.fetch_sub(
     168             517 :             1, std::memory_order_acq_rel) == 1)
     169                 :         {
     170                 :             // fetch_sub's result can be stale: a concurrent
     171                 :             // on_work_started() may raise the count before we take the
     172                 :             // lock, so re-read it here rather than trust the decrement.
     173             265 :             std::lock_guard<std::mutex> lock(mutex_);
     174             265 :             if(outstanding_work_.load(
     175             265 :                 std::memory_order_acquire) == 0 && joined_ && !stop_)
     176                 :             {
     177             137 :                 stop_ = true;
     178             137 :                 done_cv_.notify_all();
     179             137 :                 work_cv_.notify_all();
     180                 :             }
     181             265 :         }
     182             517 :     }
     183                 : 
     184                 :     void
     185             540 :     join() noexcept
     186                 :     {
     187                 :         {
     188             540 :             std::unique_lock<std::mutex> lock(mutex_);
     189             540 :             if(joined_)
     190             205 :                 return;
     191             335 :             joined_ = true;
     192                 : 
     193             335 :             if(outstanding_work_.load(
     194             335 :                 std::memory_order_acquire) == 0)
     195                 :             {
     196             147 :                 stop_ = true;
     197             147 :                 work_cv_.notify_all();
     198                 :             }
     199                 :             else
     200                 :             {
     201             188 :                 done_cv_.wait(lock, [this]{
     202             326 :                     return stop_;
     203                 :                 });
     204                 :             }
     205             540 :         }
     206                 : 
     207             704 :         for(auto& t : threads_)
     208             369 :             if(t.joinable())
     209             369 :                 t.join();
     210                 :     }
     211                 : 
     212                 :     void
     213             337 :     stop() noexcept
     214                 :     {
     215                 :         {
     216             337 :             std::lock_guard<std::mutex> lock(mutex_);
     217             337 :             stop_ = true;
     218             337 :         }
     219             337 :         work_cv_.notify_all();
     220             337 :         done_cv_.notify_all();
     221             337 :     }
     222                 : 
     223                 : private:
     224                 :     void
     225           18077 :     ensure_started()
     226                 :     {
     227           18077 :         std::call_once(start_flag_, [this]{
     228             288 :             threads_.reserve(num_threads_);
     229             657 :             for(std::size_t i = 0; i < num_threads_; ++i)
     230             738 :                 threads_.emplace_back([this, i]{ run(i); });
     231             288 :         });
     232           18077 :     }
     233                 : 
     234                 :     void
     235             369 :     run(std::size_t index)
     236                 :     {
     237                 :         // Build name; set_current_thread_name truncates to platform limits.
     238                 :         char name[16];
     239             369 :         std::snprintf(name, sizeof(name), "%s%zu", thread_name_prefix_, index);
     240             369 :         set_current_thread_name(name);
     241                 : 
     242                 :         // Mark this thread as a worker of this pool so dispatch()
     243                 :         // can symmetric-transfer when called from within pool work.
     244                 :         struct scoped_pool
     245                 :         {
     246             369 :             scoped_pool(impl const* p) noexcept { current_.set(p); }
     247             369 :             ~scoped_pool() noexcept { current_.set(nullptr); }
     248             369 :         } guard(this);
     249                 : 
     250                 :         for(;;)
     251                 :         {
     252           18281 :             continuation* c = nullptr;
     253                 :             {
     254           18281 :                 std::unique_lock<std::mutex> lock(mutex_);
     255           18281 :                 work_cv_.wait(lock, [this]{
     256           44333 :                     return !empty() ||
     257           44333 :                         stop_;
     258                 :                 });
     259           18281 :                 if(stop_)
     260             738 :                     return;
     261           17912 :                 c = pop();
     262           18281 :             }
     263           17912 :             if(c)
     264           17912 :                 safe_resume(c->h);
     265           17912 :         }
     266             369 :     }
     267                 : };
     268                 : 
     269                 : //------------------------------------------------------------------------------
     270                 : 
     271             335 : thread_pool::
     272                 : ~thread_pool()
     273                 : {
     274             335 :     impl_->stop();
     275             335 :     impl_->join();
     276             335 :     impl_->drain_abandoned();
     277             335 :     shutdown();
     278             335 :     destroy();
     279             335 :     delete impl_;
     280             335 : }
     281                 : 
     282             335 : thread_pool::
     283             335 : thread_pool(std::size_t num_threads, std::string_view thread_name_prefix)
     284             335 :     : impl_(new impl(num_threads, thread_name_prefix))
     285                 : {
     286             335 :     this->set_frame_allocator(std::allocator<void>{});
     287             335 : }
     288                 : 
     289                 : void
     290             205 : thread_pool::
     291                 : join() noexcept
     292                 : {
     293             205 :     impl_->join();
     294             205 : }
     295                 : 
     296                 : void
     297               2 : thread_pool::
     298                 : stop() noexcept
     299                 : {
     300               2 :     impl_->stop();
     301               2 : }
     302                 : 
     303                 : //------------------------------------------------------------------------------
     304                 : 
     305                 : thread_pool::executor_type
     306           11741 : thread_pool::
     307                 : get_executor() const noexcept
     308                 : {
     309           11741 :     return executor_type(
     310           11741 :         const_cast<thread_pool&>(*this));
     311                 : }
     312                 : 
     313                 : void
     314             517 : thread_pool::executor_type::
     315                 : on_work_started() const noexcept
     316                 : {
     317             517 :     pool_->impl_->on_work_started();
     318             517 : }
     319                 : 
     320                 : void
     321             517 : thread_pool::executor_type::
     322                 : on_work_finished() const noexcept
     323                 : {
     324             517 :     pool_->impl_->on_work_finished();
     325             517 : }
     326                 : 
     327                 : void
     328           17565 : thread_pool::executor_type::
     329                 : post(continuation& c) const
     330                 : {
     331           17565 :     pool_->impl_->post(c);
     332           17565 : }
     333                 : 
     334                 : std::coroutine_handle<>
     335             517 : thread_pool::executor_type::
     336                 : dispatch(continuation& c) const
     337                 : {
     338             517 :     if(pool_->impl_->running_in_this_thread())
     339               5 :         return c.h;
     340             512 :     pool_->impl_->post(c);
     341             512 :     return std::noop_coroutine();
     342                 : }
     343                 : 
     344                 : } // capy
     345                 : } // boost
        

Generated by: LCOV version 2.3