LCOV - code coverage report
Current view: top level - /jenkins/workspace/boost-root/libs/capy/src/ex/detail - strand_service.cpp (source / functions) Coverage Total Hit Missed
Test: coverage_remapped.info Lines: 100.0 % 172 172
Test Date: 2026-10-16 22:02:34 Functions: 96.3 % 27 26 1

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
       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/capy
       8                 : //
       9                 : 
      10                 : #include "src/ex/detail/strand_impl.hpp"
      11                 : #include <boost/capy/ex/detail/strand_service.hpp>
      12                 : #include <boost/capy/continuation.hpp>
      13                 : #include <boost/capy/detail/config.hpp>
      14                 : #include <coroutine>
      15                 : #include <memory>
      16                 : #include <tuple>
      17                 : #include <utility>
      18                 : 
      19                 : namespace boost {
      20                 : namespace capy {
      21                 : namespace detail {
      22                 : 
      23                 : // Sentinel stored in invoker_frame_cache_ after shutdown to prevent
      24                 : // in-flight invokers from repopulating a freed cache slot.
      25                 : inline void* const kCacheClosed = reinterpret_cast<void*>(1);
      26                 : 
      27                 : /** Concrete strand_service.
      28                 : 
      29                 :     Holds a shared mutex pool (193 entries), a linked list of live
      30                 :     impls (for shutdown traversal), and a single-slot invoker
      31                 :     coroutine frame cache shared across all strands of this service.
      32                 : 
      33                 :     The dispatch helpers (`enqueue`, `dispatch_pending`, etc.) are
      34                 :     public so the namespace-scope `make_invoker` coroutine and the
      35                 :     `strand_service` static methods can call them without friendship.
      36                 : */
      37                 : class strand_service_impl : public strand_service
      38                 : {
      39                 : public:
      40                 :     static constexpr std::size_t num_mutexes = 193;
      41                 : 
      42                 :     std::mutex mutex_;
      43                 :     std::size_t salt_ = 0;
      44                 :     std::shared_ptr<std::mutex> mutexes_[num_mutexes];
      45                 :     intrusive_list<strand_impl> impl_list_;
      46                 :     std::atomic<void*> invoker_frame_cache_{nullptr};
      47                 :     bool shutdown_ = false;
      48                 : 
      49                 :     explicit
      50 HIT        1051 :     strand_service_impl(execution_context&)
      51            1051 :     {
      52            1051 :     }
      53                 : 
      54                 :     std::shared_ptr<strand_impl>
      55           12467 :     create_implementation() override
      56                 :     {
      57           12467 :         auto new_impl = std::make_shared<strand_impl>();
      58                 : 
      59           12467 :         std::lock_guard<std::mutex> lock(mutex_);
      60           12467 :         if(shutdown_)
      61                 :         {
      62                 :             // Born closed, like the strands shutdown() closed: it
      63                 :             // borrows nothing from this service.
      64               2 :             new_impl->locked_ = true;
      65               2 :             return new_impl;
      66                 :         }
      67                 : 
      68           12465 :         std::size_t s = salt_++;
      69           12465 :         std::size_t idx = reinterpret_cast<std::size_t>(new_impl.get());
      70           12465 :         idx += idx >> 3;
      71           12465 :         idx ^= s + 0x9e3779b9 + (idx << 6) + (idx >> 2);
      72           12465 :         idx %= num_mutexes;
      73           12465 :         if(!mutexes_[idx])
      74            1684 :             mutexes_[idx] = std::make_shared<std::mutex>();
      75           12465 :         new_impl->mutex_ = mutexes_[idx].get();
      76                 : 
      77           12465 :         impl_list_.push_back(new_impl.get());
      78           12465 :         new_impl->service_.store(this, std::memory_order_release);
      79                 : 
      80           12465 :         return new_impl;
      81           12467 :     }
      82                 : 
      83                 :     // Returns the service when the caller must post an invoker.
      84                 :     static strand_service_impl*
      85          331439 :     enqueue(strand_impl& impl, continuation& c)
      86                 :     {
      87                 :         // A closed strand never locks: the mutex it borrowed may have
      88                 :         // gone with its service.
      89          331439 :         if(impl.service_.load(std::memory_order_acquire))
      90                 :         {
      91          131373 :             std::lock_guard<std::mutex> lock(*impl.mutex_);
      92          131373 :             if(auto* svc = impl.service_.load(std::memory_order_relaxed))
      93                 :             {
      94          131273 :                 impl.pending_.push(c);
      95          131273 :                 if(!impl.locked_)
      96                 :                 {
      97           18235 :                     impl.locked_ = true;
      98           18235 :                     return svc;
      99                 :                 }
     100          113038 :                 return nullptr;
     101                 :             }
     102          131373 :         }
     103                 :         // Shut down: nothing will run it, so abandon it as its holder.
     104          200166 :         capy::abandon(c);
     105          200166 :         return nullptr;
     106                 :     }
     107                 : 
     108                 :     enum class withdrawal
     109                 :     {
     110                 :         alone,
     111                 :         followed,
     112                 :         taken
     113                 :     };
     114                 : 
     115                 :     // Takes back a continuation whose invoker could not be posted.
     116                 :     // `taken` means a shutdown took it first and abandoned it.
     117                 :     static withdrawal
     118               5 :     withdraw(strand_impl& impl, continuation& c) noexcept
     119                 :     {
     120               5 :         std::lock_guard<std::mutex> lock(*impl.mutex_);
     121               5 :         if(!impl.service_.load(std::memory_order_relaxed))
     122               1 :             return withdrawal::taken;
     123                 :         // Nothing takes from the queue while it waits for an invoker,
     124                 :         // so `c` is still first.
     125               4 :         BOOST_CAPY_ASSERT(impl.pending_.front() == &c);
     126               4 :         std::ignore = c;
     127               4 :         impl.pending_.pop_front();
     128                 :         // Posts queued behind `c` returned trusting its invoker, so
     129                 :         // the strand stays locked until one runs them.
     130               4 :         if(!impl.pending_.empty())
     131               3 :             return withdrawal::followed;
     132               1 :         impl.locked_ = false;
     133               1 :         return withdrawal::alone;
     134               5 :     }
     135                 : 
     136                 :     // Gives up queued work that no invoker can be posted to run.
     137                 :     static void
     138               2 :     abandon_pending(strand_impl& impl) noexcept
     139                 :     {
     140               2 :         strand_queue::taken_batch batch;
     141                 :         {
     142               2 :             std::lock_guard<std::mutex> lock(*impl.mutex_);
     143               2 :             if(!impl.service_.load(std::memory_order_relaxed))
     144               1 :                 return;
     145               1 :             batch = impl.pending_.take_all();
     146               1 :             impl.locked_ = false;
     147               2 :         }
     148               1 :         strand_queue::abandon_batch(batch);
     149                 :     }
     150                 : 
     151                 :     static void
     152           18881 :     dispatch_pending(strand_impl& impl)
     153                 :     {
     154           18881 :         strand_queue::taken_batch batch;
     155                 :         {
     156           18881 :             std::lock_guard<std::mutex> lock(*impl.mutex_);
     157           18881 :             batch = impl.pending_.take_all();
     158           18881 :         }
     159           18881 :         impl.pending_.dispatch_batch(batch);
     160           18881 :     }
     161                 : 
     162                 :     static bool
     163           18881 :     try_unlock(strand_impl& impl)
     164                 :     {
     165           18881 :         std::lock_guard<std::mutex> lock(*impl.mutex_);
     166           18881 :         if(impl.pending_.empty())
     167                 :         {
     168           17379 :             impl.locked_ = false;
     169           17379 :             return true;
     170                 :         }
     171            1502 :         return false;
     172           18881 :     }
     173                 : 
     174                 :     static void
     175           18881 :     set_dispatch_thread(strand_impl& impl) noexcept
     176                 :     {
     177           18881 :         impl.dispatch_thread_.store(std::this_thread::get_id());
     178           18881 :     }
     179                 : 
     180                 :     static void
     181           17379 :     clear_dispatch_thread(strand_impl& impl) noexcept
     182                 :     {
     183           17379 :         impl.dispatch_thread_.store(std::thread::id{});
     184           17379 :     }
     185                 : 
     186                 :     // Defined below; needs strand_invoker complete.
     187                 :     static void
     188                 :     post_invoker(
     189                 :         strand_service_impl* svc,
     190                 :         std::shared_ptr<strand_impl> impl,
     191                 :         executor_ref ex);
     192                 : 
     193                 : protected:
     194                 :     void
     195            1051 :     shutdown() override
     196                 :     {
     197            1051 :         strand_queue::taken_batch doomed;
     198                 :         {
     199            1051 :             std::lock_guard<std::mutex> lock(mutex_);
     200            1051 :             shutdown_ = true;
     201            2065 :             while(auto* p = impl_list_.pop_front())
     202                 :             {
     203            1014 :                 std::lock_guard<std::mutex> impl_lock(*p->mutex_);
     204            1014 :                 p->locked_ = true;
     205            1014 :                 p->service_.store(nullptr, std::memory_order_release);
     206            1014 :                 auto b = p->pending_.take_all();
     207            1014 :                 if(b.head)
     208                 :                 {
     209             851 :                     if(doomed.tail)
     210               1 :                         doomed.tail->reserved = b.head;
     211                 :                     else
     212             850 :                         doomed.head = b.head;
     213             851 :                     doomed.tail = b.tail;
     214                 :                 }
     215            1014 :             }
     216            1051 :         }
     217                 :         // Outside the mutexes: a destroyed frame may drop the last
     218                 :         // strand handle and re-enter the service.
     219            1051 :         strand_queue::abandon_batch(doomed);
     220                 : 
     221            1051 :         void* fp = invoker_frame_cache_.exchange(
     222                 :             kCacheClosed, std::memory_order_acq_rel);
     223            1051 :         if(fp) ::operator delete(fp);
     224            1051 :     }
     225                 : };
     226                 : 
     227                 : /** Invoker coroutine that drains a strand's pending queue.
     228                 : 
     229                 :     Runs once the strand transitions from unlocked to locked. Holds
     230                 :     the impl alive via the coroutine parameter (a shared_ptr in the
     231                 :     coroutine frame), so user code may drop its strand handle while
     232                 :     the invoker is mid-flight.
     233                 : 
     234                 :     The frame's allocator recycles a single per-service slot. The
     235                 :     trailer points at the service (lifetime: execution_context),
     236                 :     NOT the impl (lifetime: per-strand), so operator delete is
     237                 :     safe even after the impl has been destroyed.
     238                 : */
     239                 : struct strand_invoker
     240                 : {
     241                 :     struct promise_type
     242                 :     {
     243                 :         // Stored in the coroutine frame so its address is stable for
     244                 :         // posting to the inner executor.
     245                 :         continuation self_;
     246                 : 
     247                 :         // The service is the one enqueue() saw under the strand's lock;
     248                 :         // the impl's own pointer may be cleared by a concurrent shutdown.
     249                 :         void*
     250           18238 :         operator new(
     251                 :             std::size_t n,
     252                 :             strand_service_impl* svc,
     253                 :             std::shared_ptr<strand_impl> const&)
     254                 :         {
     255           18238 :             constexpr auto A = alignof(strand_service_impl*);
     256           18238 :             std::size_t padded = (n + A - 1) & ~(A - 1);
     257           18238 :             std::size_t total = padded + sizeof(strand_service_impl*);
     258                 : 
     259                 :             // Never takes the closed marker, which would let a later
     260                 :             // delete park a frame that nothing frees.
     261           18238 :             void* p = svc->invoker_frame_cache_.load(
     262           18238 :                 std::memory_order_acquire);
     263           28603 :             while(p && p != kCacheClosed &&
     264           10365 :                 !svc->invoker_frame_cache_.compare_exchange_weak(
     265                 :                     p, nullptr, std::memory_order_acquire))
     266                 :             {
     267                 :             }
     268           18238 :             if(!p || p == kCacheClosed)
     269            7873 :                 p = ::operator new(total);
     270                 : 
     271           18238 :             *reinterpret_cast<strand_service_impl**>(
     272           18238 :                 static_cast<char*>(p) + padded) = svc;
     273           18238 :             return p;
     274                 :         }
     275                 : 
     276                 :         void
     277           18238 :         operator delete(void* p, std::size_t n) noexcept
     278                 :         {
     279           18238 :             constexpr auto A = alignof(strand_service_impl*);
     280           18238 :             std::size_t padded = (n + A - 1) & ~(A - 1);
     281           18238 :             auto* svc = *reinterpret_cast<strand_service_impl**>(
     282                 :                 static_cast<char*>(p) + padded);
     283                 : 
     284           18238 :             void* expected = nullptr;
     285           18238 :             if(!svc->invoker_frame_cache_.compare_exchange_strong(
     286                 :                     expected, p, std::memory_order_release))
     287            7844 :                 ::operator delete(p);
     288           18238 :         }
     289                 : 
     290                 :         strand_invoker
     291           18238 :         get_return_object() noexcept
     292                 :         {
     293           18238 :             return {std::coroutine_handle<promise_type>::from_promise(*this)};
     294                 :         }
     295                 : 
     296           18238 :         std::suspend_always initial_suspend() noexcept { return {}; }
     297           17379 :         std::suspend_never  final_suspend()   noexcept { return {}; }
     298           17379 :         void return_void() noexcept {}
     299                 :         void unhandled_exception() { std::terminate(); } // LCOV_EXCL_LINE terminates the process
     300                 :     };
     301                 : 
     302                 :     std::coroutine_handle<promise_type> h_;
     303                 : };
     304                 : 
     305                 : // The by-value parameter lives in the coroutine frame for the
     306                 : // invoker's lifetime, keeping the impl alive past any user-side
     307                 : // strand drop.
     308                 : static
     309                 : strand_invoker
     310           18238 : make_invoker(strand_service_impl*, std::shared_ptr<strand_impl> impl)
     311                 : {
     312                 :     auto* p = impl.get();
     313                 :     for(;;)
     314                 :     {
     315                 :         strand_service_impl::set_dispatch_thread(*p);
     316                 :         strand_service_impl::dispatch_pending(*p);
     317                 :         if(strand_service_impl::try_unlock(*p))
     318                 :         {
     319                 :             strand_service_impl::clear_dispatch_thread(*p);
     320                 :             co_return;
     321                 :         }
     322                 :     }
     323           36476 : }
     324                 : 
     325                 : void
     326           18238 : strand_service_impl::post_invoker(
     327                 :     strand_service_impl* svc,
     328                 :     std::shared_ptr<strand_impl> impl,
     329                 :     executor_ref ex)
     330                 : {
     331           18238 :     auto invoker = make_invoker(svc, std::move(impl));
     332           18238 :     auto& self = invoker.h_.promise().self_;
     333           18238 :     self.h = invoker.h_;
     334                 :     try
     335                 :     {
     336           18238 :         ex.post(self);
     337                 :     }
     338               7 :     catch(...)
     339                 :     {
     340               7 :         invoker.h_.destroy();
     341               7 :         throw;
     342               7 :     }
     343           18231 : }
     344                 : 
     345                 : // A post that throws leaves the caller holding `c`, unless a
     346                 : // shutdown already took and abandoned it.
     347                 : static
     348                 : void
     349           18235 : post_or_withdraw(
     350                 :     strand_service_impl* svc,
     351                 :     std::shared_ptr<strand_impl> const& impl,
     352                 :     executor_ref ex,
     353                 :     continuation& c)
     354                 : {
     355                 :     try
     356                 :     {
     357           18240 :         strand_service_impl::post_invoker(svc, impl, ex);
     358                 :     }
     359               5 :     catch(...)
     360                 :     {
     361                 :         using withdrawal = strand_service_impl::withdrawal;
     362               5 :         auto const w = strand_service_impl::withdraw(*impl, c);
     363               5 :         if(w == withdrawal::taken)
     364               1 :             return;
     365               4 :         if(w == withdrawal::followed)
     366                 :         {
     367                 :             try
     368                 :             {
     369               5 :                 strand_service_impl::post_invoker(svc, impl, ex);
     370                 :             }
     371               2 :             catch(...)
     372                 :             {
     373               2 :                 strand_service_impl::abandon_pending(*impl);
     374               2 :             }
     375                 :         }
     376               4 :         throw;
     377               5 :     }
     378                 : }
     379                 : 
     380           12467 : strand_impl::~strand_impl()
     381                 : {
     382           12467 :     if(auto* svc = service_.load(std::memory_order_acquire))
     383                 :     {
     384           11451 :         std::lock_guard<std::mutex> lock(svc->mutex_);
     385           11451 :         svc->impl_list_.remove(this);
     386           11451 :     }
     387                 :     // Nothing can run these any more: give them up.
     388           12467 :     auto batch = pending_.take_all();
     389           12467 :     strand_queue::abandon_batch(batch);
     390           12467 : }
     391                 : 
     392            1051 : strand_service::
     393            1051 : strand_service()
     394            1051 :     : service()
     395                 : {
     396            1051 : }
     397                 : 
     398            1051 : strand_service::
     399                 : ~strand_service() = default;
     400                 : 
     401                 : bool
     402          100017 : strand_service::
     403                 : running_in_this_thread(strand_impl& impl) noexcept
     404                 : {
     405          100017 :     return impl.dispatch_thread_.load() == std::this_thread::get_id();
     406                 : }
     407                 : 
     408                 : std::coroutine_handle<>
     409          100013 : strand_service::
     410                 : dispatch(
     411                 :     std::shared_ptr<strand_impl> const& impl,
     412                 :     executor_ref ex,
     413                 :     continuation& c)
     414                 : {
     415          100013 :     if(running_in_this_thread(*impl))
     416               3 :         return c.h;
     417                 : 
     418          100010 :     if(auto* svc = strand_service_impl::enqueue(*impl, c))
     419              11 :         post_or_withdraw(svc, impl, ex, c);
     420          100010 :     return std::noop_coroutine();
     421                 : }
     422                 : 
     423                 : void
     424          231429 : strand_service::
     425                 : post(
     426                 :     std::shared_ptr<strand_impl> const& impl,
     427                 :     executor_ref ex,
     428                 :     continuation& c)
     429                 : {
     430          231429 :     if(auto* svc = strand_service_impl::enqueue(*impl, c))
     431           18224 :         post_or_withdraw(svc, impl, ex, c);
     432          231425 : }
     433                 : 
     434                 : strand_service&
     435           12467 : get_strand_service(execution_context& ctx)
     436                 : {
     437           12467 :     return ctx.use_service<strand_service_impl>();
     438                 : }
     439                 : 
     440                 : } // namespace detail
     441                 : } // namespace capy
     442                 : } // namespace boost
        

Generated by: LCOV version 2.3