LCOV - code coverage report
Current view: top level - capy/ex - abandon.hpp (source / functions) Coverage Total Hit
Test: coverage_remapped.info Lines: 100.0 % 25 25
Test Date: 2026-10-30 19:48:36 Functions: 100.0 % 7 7

           TLA  Line data    Source code
       1                 : //
       2                 : // Copyright (c) 2026 Steve Gerbino
       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/cppalliance/capy
       9                 : //
      10                 : 
      11                 : #ifndef BOOST_CAPY_EX_ABANDON_HPP
      12                 : #define BOOST_CAPY_EX_ABANDON_HPP
      13                 : 
      14                 : #include <boost/capy/detail/config.hpp>
      15                 : #include <boost/capy/continuation.hpp>
      16                 : #include <boost/capy/ex/executor_ref.hpp>
      17                 : 
      18                 : #include <atomic>
      19                 : #include <coroutine>
      20                 : 
      21                 : namespace boost {
      22                 : namespace capy {
      23                 : 
      24                 : /** The owner of one segment of a coroutine chain.
      25                 : 
      26                 :     A segment is a run of coroutines started together: the task
      27                 :     `run_async` launches and everything it awaits, a task `run` starts
      28                 :     on another executor, one branch of `when_all` or `when_any`, or a
      29                 :     @ref quitter awaited by a coroutine that is not a quitter. Its
      30                 :     owner is the coroutine that started it, suspended above the
      31                 :     segment's top frame until that frame completes. Every coroutine in
      32                 :     the segment reaches the owner through its @ref await_context.
      33                 : 
      34                 :     When the holder of a suspended coroutine in the segment gives it up
      35                 :     with @ref abandon, the owner posts its own continuation to its
      36                 :     executor. There it unwinds the segment from the top: it destroys
      37                 :     the frame it awaits, which destroys the frames below it in turn.
      38                 :     After this teardown, the owner delivers no result. An executor that
      39                 :     destroys the posted continuation instead, as a closed one does,
      40                 :     unwinds the segment the same way.
      41                 : 
      42                 :     The owner also records why the segment was abandoned. Teardown
      43                 :     calls @ref abandon. A stopped @ref quitter calls
      44                 :     @ref abandon_for_stop instead. Its owner unwinds the segment the
      45                 :     same way, then reports the stop to its parent. An owner that never
      46                 :     asks @ref stopped treats a stop as teardown: it delivers nothing. A
      47                 :     launcher written by a user must therefore check @ref stopped if it
      48                 :     runs quitters and has to report their stops.
      49                 : 
      50                 :     @par Thread Safety
      51                 :     @ref abandon and @ref abandon_for_stop may be called from any
      52                 :     thread. Only the first call takes effect, and decides the cause; a
      53                 :     later call of either before the unwind runs does nothing.
      54                 : */
      55                 : class chain_owner
      56                 : {
      57                 :     enum : unsigned char { not_abandoned, by_teardown, by_stop };
      58                 : 
      59                 :     std::atomic<unsigned char> cause_{not_abandoned};
      60                 :     continuation self_;
      61                 :     executor_ref ex_;
      62                 : 
      63 HIT      126211 :     void abandon_as(unsigned char cause) noexcept
      64                 :     {
      65          126211 :         unsigned char expected = not_abandoned;
      66          252422 :         if(!cause_.compare_exchange_strong(
      67                 :             expected, cause,
      68                 :             std::memory_order_acq_rel,
      69                 :             std::memory_order_acquire))
      70           10002 :             return;
      71          116209 :         ex_.post(self_);
      72                 :     }
      73                 : 
      74                 : public:
      75                 :     /** Bind the owner to its coroutine and executor.
      76                 : 
      77                 :         Called by the owner before it starts the segment.
      78                 : 
      79                 :         @param self The owner's coroutine, resumed or destroyed to
      80                 :             unwind the segment.
      81                 : 
      82                 :         @param ex The executor the segment runs on.
      83                 :     */
      84          136552 :     void bind(std::coroutine_handle<> self, executor_ref ex) noexcept
      85                 :     {
      86          136552 :         self_.h = self;
      87          136552 :         ex_ = ex;
      88          136552 :     }
      89                 : 
      90                 :     /// Return true once the segment has been abandoned.
      91           32947 :     bool abandoned() const noexcept
      92                 :     {
      93           65894 :         return cause_.load(std::memory_order_acquire) != not_abandoned;
      94                 :     }
      95                 : 
      96                 :     /** Return true if the segment was abandoned because of a stop.
      97                 : 
      98                 :         @return `true` only if the first abandon of the segment was
      99                 :             @ref abandon_for_stop.
     100                 :     */
     101           30802 :     bool stopped() const noexcept
     102                 :     {
     103           61604 :         return cause_.load(std::memory_order_acquire) == by_stop;
     104                 :     }
     105                 : 
     106                 :     /** Unwind the segment on the owner's executor.
     107                 : 
     108                 :         Does nothing if the segment was already abandoned. Otherwise
     109                 :         posts the owner's continuation; nothing of the owner is touched
     110                 :         afterward, since the post may unwind and free it at once.
     111                 : 
     112                 :         @note A post that throws calls `std::terminate`.
     113                 :     */
     114          111375 :     void abandon() noexcept
     115                 :     {
     116          111375 :         abandon_as(by_teardown);
     117          111375 :     }
     118                 : 
     119                 :     /** Unwind the segment on the owner's executor, as stopped.
     120                 : 
     121                 :         The same as @ref abandon, but records the cause as a stop, so
     122                 :         @ref stopped returns `true` if this call was the first. Called
     123                 :         when a @ref quitter in the segment is stopped: at a `co_await`
     124                 :         in its body, when an operation completes into its segment's
     125                 :         gate, or when the gate through which `run_async` or `run`
     126                 :         starts a quitter does not start it.
     127                 : 
     128                 :         @note A post that throws calls `std::terminate`.
     129                 :     */
     130           14836 :     void abandon_for_stop() noexcept
     131                 :     {
     132           14836 :         abandon_as(by_stop);
     133           14836 :     }
     134                 : };
     135                 : 
     136                 : /** Give up a suspended coroutine for good.
     137                 : 
     138                 :     Called by the holder of a suspended coroutine that will never resume
     139                 :     it, such as an execution context, strand or primitive being torn
     140                 :     down. The coroutine's segment is unwound from its owner: on the
     141                 :     owner's executor, from the top frame down to this coroutine, without
     142                 :     any frame delivering a result. Ordinary cancellation of a @ref task
     143                 :     does not use this; it resumes the coroutine with an error instead.
     144                 :     A @ref quitter's stop does use abandonment, through
     145                 :     @ref chain_owner::abandon_for_stop.
     146                 : 
     147                 :     A continuation with no owner, such as an owner's own continuation,
     148                 :     is destroyed directly.
     149                 : 
     150                 :     @par Preconditions
     151                 :     `c` refers to a suspended coroutine that the caller holds and that
     152                 :     nothing else will resume or abandon. If `c` has no owner, nothing
     153                 :     awaits the coroutine: destroying its frame alone would leave any
     154                 :     awaiting frame holding a destroyed one.
     155                 : 
     156                 :     @par Postconditions
     157                 :     The caller must not touch the coroutine, or anything in its frame,
     158                 :     again. The frames may already be destroyed when this returns, if the
     159                 :     owner's executor destroys posted work at once.
     160                 : 
     161                 :     @note The unwind runs on the owner's executor, which may belong to
     162                 :     another execution context than the caller's, and may run after the
     163                 :     caller's context is destroyed. A chain that holds objects of the
     164                 :     caller's context must therefore have finished before that context
     165                 :     is torn down.
     166                 : 
     167                 :     @note Each frame is destroyed from the destructor of the frame
     168                 :     awaiting it, so the unwind uses stack in proportion to how deeply
     169                 :     the chain's coroutines are nested.
     170                 : 
     171                 :     @param c The continuation of the coroutine given up.
     172                 : */
     173                 : inline void
     174          203389 : abandon(continuation& c) noexcept
     175                 : {
     176          203389 :     if(c.owner)
     177          101372 :         c.owner->abandon();
     178          102017 :     else if(c.h)
     179          102017 :         c.h.destroy();
     180          203389 : }
     181                 : 
     182                 : } // namespace capy
     183                 : } // namespace boost
     184                 : 
     185                 : #endif
        

Generated by: LCOV version 2.3