Showing posts with label cpp20_coroutine. Show all posts
Showing posts with label cpp20_coroutine. Show all posts

Aug 1, 2026

[C++][coroutine] Symmetric Transfer - what problem it solves

The Core Problem: Stack Overflow via Asymmetric Transfer

In early C++ Coroutines (Coroutines TS), resuming a coroutine meant calling .resume() inside `await_suspend()`. When coroutines synchronously complete in a loop or tail-recurse, every .resume()  call pushes a new C++ stack frame without popping the old one, leading to stack overflow.

// ASYMMETRIC TRANSFER (Naive Approach)

void await_suspend(std::coroutine_handle<> h) {
    // Calling .resume() pushes a new stack frame.
    // If done in a deep loop or recursive chain, stack space explodes!
    other_coro_.resume(); 
}


Stack Frame Accumulation:

[ loop_coroutine$resume ]

  └─> [ task::awaiter::await_suspend ]

        └─> [ child_coroutine$resume ]

              └─> [ final_awaiter::await_suspend ]

                    └─> [ loop_coroutine$resume ]  <-- STACK OVERFLOW!



The Solution: Symmetric Transfer

Symmetric transfer allows `await_suspend()` to return a std::coroutine_handle<> instead of void.


Returning a handle suspends the current coroutine frame, pops the current stack frame, and transfers execution directly to the returned handle via a tail call.

Stack usage remains O(1) regardless of how many synchronous suspension/resumes occur.

// SYMMETRIC TRANSFER (Modern C++20)

std::coroutine_handle<> await_suspend(std::coroutine_handle<> h) {
    // Return the handle to transfer control directly.
    // The compiler generates a tail-call: pops current stack frame, then resumes target.
    return other_coro_; 

}


Key Implementations

A. The Awaiter (`task::operator co_await`)

When `co_await child_task;` executes, transfer control directly to the child's handle:

struct task_awaiter {
    std::coroutine_handle<promise_type> child_coro_;
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns child handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<> awaiting_coro) noexcept {
        // 1. Store caller as continuation in child's promise
        child_coro_.promise().continuation = awaiting_coro;
        
        // 2. Return child handle -> tail-call into child_coro_
        return child_coro_; 

    }
    void await_resume() noexcept {}
};


B. The Final Suspend (`promise_type::final_suspend`)

When a child coroutine finishes at `co_return`, transfer control back to its continuation (the caller):

struct final_awaiter {
    bool await_ready() noexcept { return false; }

    // Symmetric Transfer: Returns caller's handle to resume
    std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> me) noexcept {
        // Returns parent handle -> tail-call back to parent coroutine
        return me.promise().continuation; 
    }
    void await_resume() noexcept {}

};

struct promise_type {
    std::coroutine_handle<> continuation{std::noop_coroutine()};
    final_awaiter final_suspend() noexcept { return {}; }
    // ...

};

Summary Matrix



[C++] coroutine memory layout


Dec 27, 2025

[C++] coroutine cheat sheet - 3

Reference:
[C++] Object Lifetimes reading minute



When HALO (Heap Allocation Elision Optimization) could happen.

  1. The Lifetime is Strictly Nested
    The compiler must be able to prove that the coroutine's lifetime ends before the caller's execution finishes.
    If the coroutine object (the "handle" or "task") is returned and its destruction point cannot be determined at compile-time, the compiler must play it safe and use the heap.
  2. The Coroutine State Size is Known
    The compiler needs to know exactly how much space the coroutine requires (including captured variables and promise objects) at the call site.
    This usually requires the coroutine body to be visible to the compiler
    (i.e., in the same translation unit or available via Link Time Optimization).
  3. Use of std::get_return_object_on_allocation_failure (Optional but relevant)
    If our promise_type defines this static member function, it signals to the compiler how to handle allocation failures.
    While this doesn't force an opt-out, it changes the allocation strategy to be more robust.

The HALO Optimization Process:

The optimization works roughly like this:

  • Analysis: The compiler looks at the co_await and destruction points of the coroutine object.
  • Inlining: It attempts to inline the coroutine logic into the caller.
  • Elision: If the compiler sees that the coroutine state does not "escape" the function, it replaces operator new with a local stack allocation.

How to Encourage the Compiler to Opt-Out

Since we cannot explicitly keywords like noheap, we have to "help" the compiler's optimizer:
  • Keep the coroutine local: Avoid passing the coroutine handle to other threads or storing it in global containers.
  • Enable High Optimization: HALO typically requires -O2 or -O3 (GCC/Clang) or /O2 (MSVC).
  • Inline the Coroutine: Define the coroutine in a header or the same file where it is called so the compiler can see the full lifecycle.
Task my_coroutine() {
    co_return 42;
}

void caller() {
    auto t = my_coroutine(); // Compiler can see 't' lives only here
    // ... do something ...
} // 't' is destroyed here; Heap allocation likely elided. 


Limitations

  • Dynamic Dispatch: If we call a coroutine through a virtual function or a function pointer, the compiler usually cannot perform HALO.
  • Tail Calls: Complex chains of coroutines can sometimes make it difficult for the compiler to prove nested lifetimes.

The "Manual" Opt-Out (Custom Allocators)

If we cannot rely on the compiler's optimization (e.g., in embedded systems), 
we can manually opt-out of the default heap by overloading operator new in our promise_type.

Using a Static Buffer or Arena:

We can provide a custom operator new that pulls from a pre-allocated memory pool or a stack-based arena, effectively bypassing the system heap.
struct promise_type {
    // Overloading new allows we to use a custom allocator
    void* operator new(std::size_t size) {
        return my_custom_arena.allocate(size);
    }
    void operator delete(void* ptr) {
        my_custom_arena.deallocate(ptr);
    }
    // ... other promise members
};


How to Verify if Elision Happened

Since HALO is an optimization, it can be fragile. We can verify it using these tricks:
  • Print in Custom New: Add a printf inside our promise_type::operator new. If it doesn't print during execution at -O3, the compiler successfully elided the call.
  • Compiler Explorer (Assembly): Check for the absence of call operator new or malloc in the generated assembly.
  • Clang-Specific Attributes: Clang is experimenting with attributes like [[clang::coro_inplace_task]] to make this elision more deterministic, though this is not standard C++20. (reference: Language Extension for better, more deterministic HALO for C++ Coroutines)
#include <coroutine>
#include <iostream>
#include <array>

struct StaticTask {
    struct promise_type {
        // 1. Intercept the arguments of the coroutine function
        // This allows 'operator new' to see the buffer passed to the coroutine
        void* operator new(std::size_t size, std::span<std::byte> buffer) {
            if (size > buffer.size()) {
                throw std::bad_alloc();
            }
            std::cout << "Allocating " << size << " bytes from stack buffer\n";
            return buffer.data();
        }

        // Must provide a matching delete (even if it does nothing)
        void operator delete(void*, std::size_t) {}

        StaticTask get_return_object() { return {std::coroutine_handle<promise_type>::from_promise(*this)}; }
        std::initial_suspend initial_suspend() { return {}; }
        std::final_suspend final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };

    std::coroutine_handle<promise_type> handle;
};

// Usage
StaticTask my_coro(std::span<std::byte> storage) {
    std::cout << "Coroutine running!\n";
    co_return;
}

int main() {
    std::array<std::byte, 1024> stack_space; 
    auto task = my_coro(stack_space); // Passes the buffer to 'operator new'
}

In C++20, the operator new for a coroutine is uniquely powerful because the compiler performs a special lookup.
It doesn't just look for a standard void* operator new(size_t); it looks for an overload that matches the entire signature of our coroutine function.

Why it takes the buffer as an argument
When we call a coroutine like my_coro(some_buffer), the compiler needs to allocate space for the "coroutine frame" 
(which holds local variables and the promise).

To give us total control, the C++ standard says:

The compiler will first try to find an operator new in our promise_type that takes (std::size_t, Args...),
where Args... are the exact types passed to the coroutine function.

If it finds this "matching" version, it calls it and passes the arguments we provided in the function call.

This is the "magic hook" that allows us to pass a specific memory source (like a stack-based span or a custom Arena&) directly into the allocation logic.

The Lookup Mechanics

The compiler follows this priority list when it sees a coroutine call:

Priority   Signature the compiler looks for Description
  1. (Best)  operator new(size_t, P1, P2...)
    Takes the size plus all coroutine arguments (P1,P2).
  2. operator new(size_t)                   
    The standard class-specific allocator.
  3. ::operator new(size_t)                 
    Global heap allocation. (Fallback)
Note: For member functions, the first argument after size_t is actually the this pointer, followed by the function arguments.



struct promise_type {
    std::span<std::byte> m_buffer;

    // 1. Used to POSITION the memory (Allocation)
    void* operator new(std::size_t size, std::span<std::byte> buffer) {
        if (size > buffer.size()) throw std::bad_alloc();
        return buffer.data();
    }

    // 2. Used to INITIALIZE the promise (Construction)
    // The compiler sees that the coroutine was called with a span,
    // so it looks for a constructor that accepts it.
    promise_type(std::span<std::byte> buffer) : m_buffer(buffer) {
        std::cout << "Promise initialized with buffer of size " << m_buffer.size() << "\n";
    }

    // ... rest of promise_type members ...
};

[[clang::coro_await_elidable_argument]] 

attribute is a specialized Clang-specific optimization hint designed to reduce the overhead of asynchronous programming. It is primarily used to enable HALO (Heap Allocation Elision Optimization) for coroutines.

This attribute is applied to a parameter of a function (usually an operator await). It tells the compiler that the coroutine being passed as an argument is a temporary that will not outlive the current function. This gives the compiler a "green light" to:
  • Elide the heap allocation: Instead of putting the coroutine frame on the heap, it puts it on the caller's stack.
  • Inline the coroutine: It allows for better devirtualization and inlining of the coroutine's lifecycle.

Sep 28, 2025

[C++] coroutine cheat sheet - 2

Reference:
[C++] coroutine cheat sheet - 1
https://reductor.dev/cpp/2023/08/10/the-downsides-of-coroutines.html#how-normal-functions-and-the-stack-work

Rule

  1. Do not resume caller's handler inside a mem_fn that returns value/handler.
    otherwise the return will be suspended in the callee.
    reference:
    https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer
    A pattern, however, the caller's awaitable::await_suspend() could call callee's handler to resume,
    while callee's promise::final_suspend() could call caller handler to resume() and back to
    caller's awaitable::await_suspend().
  2. Control the suspend from mem_fn that it's signature returns awaitable type.
  3. Store {yeild values/caller's handler} inside the promise instance.
  4. Make sure promise::final_suspend() is run before caller resume.
    This indicates, the caller::handler::resume() can only be called
    inside the promise::final_suspend() returned awaitable::await_suspend() and awaitable::await_ready() should return false to trigger the run of awaitable::await_suspend()
  5. If promise::final_suspend() returns std::suspend_never{}, the coroutine heap is destroyed automatically/immidiately.
    If promise::final_suspend() returns other then std::suspend_never{} and
    has the awaitable::await_ready() returns false, the user defined coroutine
    handler's destructor should call detroy() manually to release the heap.
  6. Thread local variables should not be used in coroutines to prevent buggy logic.
    https://rules.sonarsource.com/cpp/RSPEC-6367
  7. std::coroutine_handle<Promise>::operator()std::coroutine_handle<Promise>::resume
    The behavior is undefined if *this does not refer to suspended coroutine, or the coroutine is not a no-op coroutine and suspended at its final suspend point. A concurrent resumption of the coroutine may result in a data race.
Coroutine defined as:
task<void> func()
{
  int mylocal = 10;
  co_return;
}
Compiler generated coroutine code would be like:
// Struct representing the coroutine state
struct func_frame
{
  task<void>::promise_type __promise;
  int __step = 0;

  decltype(__promise.initial_suspend()) __initial_suspend_awaiter;
  decltype(__promise.final_suspend()) __final_suspend_awaiter;

  // Structure to hold local and temporary variables
  struct 
  {
    // Local and temporary variables reside here
    int mylocal;
  } local_and_temps;

  // Function to resume coroutine execution
  void resume()
  {
    switch(__step)
    {
      case 0:
        // co_await __promise.initial_suspend();
        __initial_suspend_awaiter = __promise.initial_suspend();
        if (!__initial_suspend_awaiter.await_ready())
        {
          __step = 1;
          __initial_suspend_awaiter.await_suspend();
          return;
        }
      case 1:
        __initial_suspend_awaiter.await_resume();
        // .. func body
        mylocal = 10;
        // co_return
        __promise.return_void();
        // co_await __promise.final_suspend();
        __final_suspend_awaiter = __promise.final_suspend();
        if (!__final_suspend_awaiter.await_ready())
        {
          __final_suspend_awaiter.await_suspend();
          return;
        }
        delete this;
    }
  }
};

// Coroutine function transformed into a coroutine frame
task<void> func()
{
  func_frame * frame = task<void>::promise_type::operator new(func_frame);
  task<void> ret = frame->__promise.get_return_object()
  frame->resume();
  return ret;
}
Which involves multiple function calls:
  • func()
  • promise_type::operator new()
  • promise_type::promise_type()
  • promise_type::get_return_value()
  • promise_type::initial_suspend()
  • initial_suspend::initial_suspend()
  • initial_suspend::await_ready()
  • initial_suspend::await_suspend() [opt]
  • initial_suspend::await_resume()
  • promise_type::return_void()
  • final_suspend::final_suspend()
  • final_suspend::await_ready()
  • final_suspend::await_suspend() [opt]
  • promise_type::operator delete()

Code explains:
#include <coroutine>
#include <iostream>

/**
Rule:
1. Do not resume caller's handler inside a mem_fn that returns value/handler.
  otherwise the return will be suspended in the callee.
2. Control the suspend from mem_fn that it's signature returns awaitable type.
3. store {yeild values/caller's handler} inside the promise instance.
4. make sure promise::final_suspend() is run before caller resume.
  this indicates, the caller::handler::resume() can only be called
  inside the promise::final_suspend() returned awaitable::await_suspend() and
  awaitable::await_ready() should return false to trigger the run of
  awaitable::await_suspend()
5. If promise::final_suspend() returns std::suspend_never{}, the coroutine heap
  is destroyed automatically.
  If promise::final_suspend() returns other then std::suspend_never{} and
  has the awaitable::await_ready() returns false, the user defined coroutine
handler's destructor should call detroy() manually to release the heap.
*/
using namespace std;

enum class TaskType : char {
  foo,
  bar,
};

template <TaskType ttype> struct Task;

// promise_type
template <TaskType ttype> struct promise_type {
  Task<ttype> get_return_object();
  std::suspend_always initial_suspend();

  auto final_suspend() noexcept;

  void return_value(int);
  void unhandled_exception();
  TaskType get_ttype() { return ttype; }
  int ret_val;
  std::coroutine_handle<promise_type<TaskType::bar>> promise_type_handle_;
};
// End promise_type

template <TaskType ttype>
struct Task : std::coroutine_handle<promise_type<ttype>> {
  using promise_type = promise_type<ttype>;
  // Awaitable interface definition
  bool await_ready() {
    std::cout << "await_ready: false\n";
    return false;
  }

  std::coroutine_handle<promise_type>
  await_suspend(std::coroutine_handle<::promise_type<TaskType::bar>> handler) {
    std::cout << "await_suspend receive handler pnted to type: "
              << (int)handler.promise().get_ttype()
              << " and this task's type: " << (int)this->promise().get_ttype()
              << "\n";

    // using this due to 13.4.2 Dependent Base Classes
    this->promise().promise_type_handle_ = handler;
    return *this;
  }

  int await_resume() {
    std::cout << "await_resume; type: " << (int)ttype << "\n";
    return this->promise().ret_val;
  }
  // End Awaitable interface definition

  ~Task() {
    std::cout << "Task destructor called, type: " << (int)ttype << "\n";
    this->destroy();
  }

  TaskType get_ttype() { return ttype; }

  // If has default Task constructor, this `return
  // {Task<ttype>::from_promise(*this)};` won't compile.
  // Task() { std::cout << "task construct type: " << (int)ttype << "\n"; }

}; // Task

// promise_type mem_fn definition
template <TaskType ttype> Task<ttype> promise_type<ttype>::get_return_object() {
  std::cout << "get_return_object type: " << (int)ttype << "\n";
  return {Task<ttype>::from_promise(*this)};
};

template <TaskType ttype>
std::suspend_always promise_type<ttype>::initial_suspend() {
  std::cout << "initial_suspend always\n";
  return {};
}

template <TaskType ttype> auto promise_type<ttype>::final_suspend() noexcept {
  std::cout << "final_suspend, this promise type: " << (int)ttype << "\n";
  // Bar's handler
  // if (promise_type_handle_) {
  //   std::cout << "resume bar's handler\n";
  //   promise_type_handle_.resume();
  // }
  struct tmp_awaitable {
    bool await_ready() noexcept {
      std::cout << "tmp_awaitable await_ready false\n";
      return false;
    }

    std::coroutine_handle<> await_suspend(
        std::coroutine_handle<::promise_type<TaskType::foo>> handler) noexcept {
      std::cout << "tmp_awaitable await_suspend\n";
      if (handler.promise().promise_type_handle_) {
        return handler.promise().promise_type_handle_;
      }
      std::cout << "tmp_awaitable await_suspend after resume\n";
      return std::noop_coroutine();
    }

    void await_resume() noexcept {
      std::cout << "tmp_awaitable await_resume\n";
    }
  };

  if constexpr (ttype == TaskType::foo) {
    std::cout << "final_suspend type: " << (int)ttype << " suspend never\n";
    // return std::suspend_never{};
    return tmp_awaitable{};
  } else {
    std::cout << "final_suspend type: " << (int)ttype << " suspend always\n";
    return std::suspend_always{};
  }
}

template <TaskType ttype> void promise_type<ttype>::return_value(int val) {
  std::cout << "return_val: " << val << "\n";
  ret_val = val;
  // promise_type_handle_.resume();
}

template <TaskType ttype> void promise_type<ttype>::unhandled_exception() {
  std::cout << "unhandled_exception\n";
}
// End promise_type mem_fn definition

Task<TaskType::foo> foo() {
  std::cout << "foo func start\n";
  co_return 42;
}

Task<TaskType::bar> bar() {
  std::cout << "bar func start\n";
  // co_return;
  // co_await foo();
  int val = co_await foo(); // Foo's task destructs here after co_await returns;
  std::cout << "bar func end val: " << val << "\n";
}

int main() {
  std::cout << "call bar\n";
  auto btask = bar();
  std::cout << "get btask and resume bar handle: " << (int)btask.get_ttype()
            << "\n";
  btask.resume();
  std::cout << "main-end\n";
  /*
        start call bar
        initial_suspend
        main-end
   */
}

Aug 3, 2025

[C++] coroutine cheat sheet

Key components:

  • Return type
  • Promise type
  • Awaitable type
  • std::coroutine_handle<>

Clang Attributes:

task<int> coro(const int& a) { co_return a + 1; }
task<int> dangling_refs(int a) {
  // `coro` captures reference to a temporary. `foo` would now contain a dangling reference to `a`.
  auto foo = coro(1);
  // `coro` captures reference to local variable `a` which is destroyed after the return.
  return coro(a);
}

template <typename T> struct [[clang::coro_return_type, clang::coro_lifetimebound]] Task {
  using promise_type = some_promise_type;
};

Task<int> coro(const int& a) { co_return a + 1; }
[[clang::coro_wrapper]] Task<int> coro_wrapper(const int& a, const int& b) {
  return a > b ? coro(a) : coro(b);
}
Task<int> temporary_reference() {
  auto foo = coro(1); // warning: capturing reference to a temporary which would die after the expression.

  int a = 1;
  auto bar = coro_wrapper(a, 0); // warning: `b` captures reference to a temporary.

  co_return co_await coro(1); // fine.
}
[[clang::coro_wrapper]] Task<int> stack_reference(int a) {
  return coro(a); // warning: returning address of stack variable `a`.
}

Important performance to avoid temporary object being created during pass by value
but resides in the register
https://vsdmars.blogspot.com/2021/01/c-trivialabi-and-borrow-ownership.html

Attribute trivial_abi has no effect in the following cases:
  • The class directly declares a virtual base or virtual methods.
  • Copy constructors and move constructors of the class are all deleted.
  • The class has a base class that is non-trivial for the purposes of calls.
  • The class has a non-static data member whose type is non-trivial for the purposes of calls, which includes:
    • classes that are non-trivial for the purposes of calls
    • __weak-qualified types in Objective-C++
    • arrays of any of the above

Concept:

`co_yield value` is a syntax sugar for `co_await awaitable`.
thus value is stored in promise_type instance directly instead of having to go through
`Awaitable{value}` and store the value into promise_type instance through `Awaitable::await_suspend(handler)`

Caller could retrieve the value through
`ReturnType::handler_::promise.value_;`

std::suspend_always {
  // always suspend.
  bool await_ready() { return false;}
  // noop
  void await_suspend()
  // noop, the value is stored in promise_type.value_.
  void await_resume()
} // https://en.cppreference.com/w/cpp/coroutine/suspend_always.html

std::suspend_never {
  // never suspend.
  bool await_ready() { return true;}
  // noop
  void await_suspend()
  // noop, the value is stored in promise_type.value_.
  void await_resume()
}; // https://en.cppreference.com/w/cpp/coroutine/suspend_never.html

// Controls caller's behavior. Thus await_suspend is passing in caller's
// coroutine handler. However, the Awaitable instance is created by the
// callee, which it could store the callee's coroutine handler.
strict Awaitable {
  T value_;
  bool await_ready() { return false;}
  void await_suspend(std::coroutine_handle<promise_type> handler) {
    // could store the result into handler.promise().RESULT.
    //    or store the value_ into handler.promise().RESULT.
    // Handler can be passed into new thread.
    // When handler calls .resume(), it starts execute from where previous
    // suspended.
    // These await_* functions controlls how the caller, i.e. not the callee that
    // provides this Awaitable but the caller, behave.
    // if returns false, indicates the caller will continue and
    // await_resume is called immidiately and the result is returned to the caller.
    // if returns true, the caller is suspended and the callee only resume
    // if callee's handler::resume() is called. otherwise, callee and caller both
    // are suspended and return back to main()/root caller function(i.e. the caller function
    // that is not a coroutine.
  }

  // handler.resume() from Awaitable suspend comes here.
  // can return value to the caller inside the coroutine.
  // the value can be obtained from handler.promise().RESULT.
  void await_resume();
  T await_resume() {return T{}; };

  Awaitable(T value) : value_(value) {}
};



struct ReturnType {
  struct promise_type {
    T value_;
    promise_type(T...); // optional

    ReturnType get_return_object() {
      return {};
      // or
      return {coroutine::from_promise(*this)};
    };

    std::suspend_always initial_suspend() {}

    template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from) {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
    }

    // above start; below shutdown

    void return_value(T/T&&/const T&); // store value T inside the ReturnType, caller retrieve the value from ReturnType. 
    void return_void();

    void unhandled_exception();
    
    // 1) if returns suspend_always; the caller has the chance to control how coroutine is going
    //     to be clean-up, e.g. release lock, release resources etc. and then call .destroy()
    //     manually, usually in the ReturnType's destructor. After .destroy() is called,
    //     std::coroutine_handle::operator bool continue to return true, this is due
    //     to coroutine_handle does initial with a coroutine(that it points to).
    //     And If std::coroutine_handle::destroy()
    //     is called, std::coroutine_handle::done() will have undefined behavior and should not be called.
    // 2) if returns suspend_never; the coroutine is destroyed immediately, and calling 
    //    std::coroutine_handle::done() is UB since frame is destroyed.
    // 3) if suspend_always, the coroutine is suspended and std::coroutine_handle::done() is True.
    std::suspend_always final_suspend() noexcept;
  };

  ReturnType(std::coroutine_handle<promise_type> handler) : handler_(handler) {}

  ReturnType() = default;
};

Jul 29, 2025

[C++] Coroutine examples

  1. C++ coroutine has two scopes: caller scope, coroutine scope (if you familiar with Python yield/send, same concept but more subtle with object lifetime and more flexible. Or Golang goroutine/channel [stackful], or C++ fiber[stackful])
  2. Suspend suspends the coroutine and back to the caller, caller uses handler to resume the coroutine
  3. Underneath using JMP instead of CALL since it's stackless coroutine.
  4. Beware that even `void coroutine(const int& arg)` that has co_await/co_yield/co_return, when it suspends, the binding arg has been stack rewinded thus destroyed.
    i.e.
    If a coroutine has a parameter passed by reference, resuming the coroutine after the lifetime of the entity referred to by that parameter has ended is likely to result in undefined behavior.

    The C++ core guidelines say not to use references at all.
  5. Just avoid references" isn't comprehensive, e.g. we have span, string_view, even though they are not reference, can still facing the same dangling issue after co_await/co_yield/co_return.
    Thus, a better contract is:
    Pass by Value with Owning Types.
  6. The way we ensure caller doesn't compile is by making Co non-moveable, and accepting it by value in the co_await implementation in the promise type.

    This means the only way to await it is if you do so immediately in the same full expression as the function call, so that guaranteed copy elision can kick in. 

    Because temporaries aren't destroyed until that full expression has been evaluated, the lifetimes work out perfectly using the usual language rules around lifetimes.

    Design TIP:
    Eliminated the problem with references by just declaring an entire pattern of code illegal.
      Co<void> UseInt_Async(const int& x) {
         printf("%d\n", x);
         co_await DoSomething();
         printf("%d\n", x);
      }
      
      k3::Co<void> UseInt_Async(const int&);
      // Works fine
      co_await UseInt_Async(17);
        
      // Compiler error due to k3::Co<void> is not moveable.
      k3::Co<void> co = UseInt_Async(17);
      co_await std::move(c);
  7. What if we want to use above pattern?
    Indirection, just like std::bind
    k3::Co<void> UseInt_Async(const int&);
    // Totally safe
    k3::Future<void> future{UseInt_Async, 17};
    printf("I created the future!\n");
    co_await std::move(future).Run();
  8. Co<void> has the interface concept of awaitable, as below.
  9.  Fan out or run in sequence; the implementation is imaginable.
      
    // Run all concurrently, finishing once all finish.
    k3::Co<void> FanOut(std::vector<k3::Future<void>> futures);
    // Run all concurrently, finishing when the first finishes.
    template <typename T>
    k3::Co<T> Race(std::vector<k3::Future<T>> futures);
    

  10. co_await awaitable;
    struct awaitable {
    	bool await_ready() { return false; // false: suspended, true: not suspended}
    	void await_suspend(std::coroutine_handle<> h) { // what to do when suspended}
    	void await_resume() { // in await_suspend's coroutine_handle calling .resume() comes here. and after this resume to coroutine.  }
        int await_resume() {return 42; // co_await returns value from here.}
    }; 
    co_yield
    co_return

Stack:

Go goroutines has dynamic stack size, default to 2kb. linux has thread size default to 8mb, 64k in production.
So in coroutine, how do we avoid stack overflow?

tail call comes to the rescue;
c++ coroutine guarantees a tail call into the other coroutine, and then you can do it again on the way back out. There's no need to have a stack frame to return to; everything is a JUMP instruction.
This is the only guaranteed[as of c++20] tail call in the standard; it's actually kind of a unique mixing of abstraction levels.
Co<void> Foo();
Co<void> Bar() {
  // Tail call into the body of Foo.
  co_await Foo();
  // Tail call back from the body of Foo.
  [...]
}
This indicates that with coroutine embedded inside a coroutine all use the same stack; thus
if there are multiple suspension, the stack can be overflown.

c9 solution:
we are never allowed to have one coroutine directly resume another.

Possible solution to the Notify/Wait pattern:
class Event {
Co<void> Wait();
// If there is a waiter, it will start running concurrently
// on another thread. The calling thread continues on.
// Look at the co_wait example below.
void Notify();
};
We could resolve this by breaking the assumption that there is only one thread available.
Instead we could resume the coroutine on a different thread, letting it run concurrently at the same time as the notifier.


Avoid executors in the coroutine library design:

Don't offer unnecessary configurability.
library should be agnostic to executors. All it has is its one thread-local queue of things that have been resumed by the running coroutine.
Of course individual things you wait for might have an executor internally. Like if you wait for an RPC to finish, you'll probably be resumed on one of a team of threads reading RPC replies from the network.
e.g.


// Hop to a specific executor. We need to run there because…
// look the co_await example below~
co_await Reschedule(my_executor);

Cancellation in library design

  • RPCs to stuck machines
  • Request hedging (idempotent operations)
  • Avoiding wasted work
  • Timeouts

The callee exists only to serve the caller. It must stop promptly if the caller loses interest.

  • the caller of a coroutine is always in control.
  • If the caller no longer wants the callee to run, the callee should stop running.
  • And it should do so promptly. Not after getting a response to its RPC. Not after a timeout expires. Immediately.
    All it should get to do is the kind of thing you want to do before you release a lock or unwind after an exception. Ensure internal invariants are restored; that kind of thing.
  • coroutine's local automatic variable won't be destructed until it is resumed till the end of the coroutine. (while is is on the heap)
  • If cancellation happens (which is to resume after coroutine suspended, and pass the cancellation bit to the coroutine_handler which be consumed by the awaitable, and the awaitable resume checks the bit to cancel the original code path.), the coroutine continues till the end.

Notes on structured concurrency; idea:
Children should not out-live parent.
Prefer structured concurrency wherever possible.
Good API design also helps with usability and safety.

Handler:

// task, aka handler, will be created through promise_type instance's get_return_object()
// and destructed once the coroutine function is returned to the caller.
// the destruct of task does not mean the promise_type instance is destructed, it still resides on the heap where
// coroutine is located.
struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; // Always called first when coroutine being called. }
        std::suspend_never initial_suspend() { return {}; // Always called second when coroutine being called.   }
        std::suspend_never final_suspend() noexcept { return {}; / Always called last when coroutine is finished. (i.e. done)  }
        void return_void() { // called if return from coroutine with void.}
        std::suspend_always yield_value(T value) noexcept
        {
          // used for co_yeild, once co_yeild is called, the yield value is installed here.
          // and suspend afterwards due to return `std::suspend_always`
          return {};
        }
        
        std::suspend_always return_value(T value) {
          // used for co_return, once co_return is called with expr, the expr value is installed here.
          // and suspend afterwards due to return `std::suspend_always`
          return {};
        }
        void unhandled_exception() {}
    };
    
    task(std::coroutin_handler<promise_type> h) {
      // store the coroutin_handler inside the task.
    }
};

#include <iostream>
#include <coroutine>

// 1. The Promise Type
struct MyTaskPromise {
    // The compiler calls this to get the return object.
    // We return a 'MyTask' object, and in its constructor, we pass it the handle.
    struct MyTask get_return_object();

    std::suspend_never initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() noexcept {}
    void unhandled_exception() noexcept {}
};

// 2. The Task Type
// This is the wrapper around the coroutine_handle.
struct MyTask {
    using promise_type = MyTaskPromise;
    std::coroutine_handle<MyTaskPromise> handle;

    // The constructor takes the handle from the promise's get_return_object() call.
    MyTask(std::coroutine_handle<MyTaskPromise> h) : handle(h) {}
};

// Now we can define get_return_object() because MyTask is defined.
MyTask MyTaskPromise::get_return_object() {
    // This is the key line: we construct the MyTask object with the handle
    // to the coroutine that owns this promise.
    return MyTask{std::coroutine_handle<MyTaskPromise>::from_promise(*this)};
}

// 3. A simple awaitable to demonstrate suspension.
struct Awaitable {
    bool await_ready() { return false; }
    void await_resume() {}

    // This await_suspend returns `true`, which suspends the coroutine
    // and returns control to the caller (main).
    bool await_suspend(std::coroutine_handle<>) noexcept {
        std::cout << "-> Awaitable: Coroutine is suspending." << std::endl;
        return true;
    }
};

// 4. The Coroutine Function
MyTask MyCoroutine() {
    std::cout << "Coroutine: Starting." << std::endl;
    co_await Awaitable{}; // Coroutine suspends here.
    std::cout << "Coroutine: Resumed and finishing." << std::endl;
    co_return;
}

// 5. The Caller (main function)
int main() {
    // 1. The call to MyCoroutine() returns a 'MyTask' object.
    // This object contains the handle to the suspended coroutine.
    MyTask task = MyCoroutine();

    std::cout << "\nMain: Coroutine is suspended. I am the caller.\n" << std::endl;

    // 2. We can now use the handle stored inside the 'task' object
    // to resume the coroutine.
    if (task.handle) {
        std::cout << "Main: Resuming the suspended coroutine." << std::endl;
        task.handle.resume();
    }

    std::cout << "\nMain: Coroutine has finished its execution." << std::endl;

    // 3. Clean up the coroutine's memory.
    if (task.handle) {
        task.handle.destroy();
    }

    return 0;
}



co_yield Example:

#include <coroutine>
#include <iostream>
#include <optional>

template<std::movable T>
class Generator
{
public:
    struct promise_type
    {
        Generator<T> get_return_object()
        {
          std::cout << "get_return_object()\n"; // -2; Generator created with pointer to the heap
          return Generator{Handle::from_promise(*this)};
        }
        static std::suspend_always initial_suspend() noexcept
        {
          std::cout << "suspend_always initial_suspend()\n"; // -4 init. suspend. Go to the caller.
          return {};
        }
        static std::suspend_always final_suspend() noexcept
        {
          std::cout << "suspend_always final_suspend()\n"; // -15 coroutine ends, call this and suspend.
          // Back to the caller. Go to (13)
          return {};
        }
        std::suspend_always yield_value(T value) noexcept
        {
          std::cout << "yield_value() : " << value << "\n"; // -9, suspend. Go to the caller.
          current_value = std::move(value);
          return {};
        }
        // Disallow co_await in generator coroutines.
        void await_transform() = delete;
        [[noreturn]]
        static void unhandled_exception() { throw; }

        std::optional<T> current_value;
    };

    using Handle = std::coroutine_handle<promise_type>;

    explicit Generator(const Handle coroutine) :
        m_coroutine{coroutine}
    {
      std::cout << "Generator constructor\n"; // -3
    }

    Generator() = default;
    ~Generator()
    {
        // make sure no double free through handler.destroy() while
        // caller could have a copy of the handler.
        if (m_coroutine && !m_coroutine.done())
            m_coroutine.destroy();
      std::cout << "Generator destructor\n";
    }

    Generator(const Generator&) = delete;
    Generator& operator=(const Generator&) = delete;

    Generator(Generator&& other) noexcept :
        m_coroutine{other.m_coroutine}
    {
      std::cout << "Generator move constructor\n";
      other.m_coroutine = {};
    }
    Generator& operator=(Generator&& other) noexcept
    {
      std::cout << "Generator assign operator=\n";
        if (this != &other)
        {
            if (m_coroutine)
                m_coroutine.destroy();
            m_coroutine = other.m_coroutine;
            other.m_coroutine = {};
        }
        return *this;
    }

    // Range-based for loop support.
    class Iter
    {
    public:
        void operator++()
        {
          std::cout << "Iter ++ currnet value: " << *m_coroutine.promise().current_value << "\n";
          
          m_coroutine.resume(); // -11, resume from (9) yeild's suspend; JUMP to COROUTIN RIGHT AWAY!
          // Following cout is not run until coroutin suspend again.
          
          // -13, followed by (12) and after (9) yeild's suspend.
          std::cout << "Iter ++ resumed currnet value: " << *m_coroutine.promise().current_value << "\n";
        }
        const T& operator*() const
        {
          // -10, caller print out the value.
          std::cout << " Iter* return value: " << *m_coroutine.promise().current_value << "\n";
          return *m_coroutine.promise().current_value;
        }
        bool operator==(std::default_sentinel_t) const
        {
          // -16, caller is calling this from coroutin's suspend.
          std::cout << "Iter == called: !m_coroutine: " << (!m_coroutine) << " m_coroutine.done(): " << m_coroutine.done() << "\n";
            return !m_coroutine || m_coroutine.done();
        }

        explicit Iter(const Handle coroutine) :
            m_coroutine{coroutine}
        {}

    private:
        Handle m_coroutine;
    };

    Iter begin()
    {
      std::cout << "Iter begin\n"; // -5, caller range for calls begin()
        if (m_coroutine)
            m_coroutine.resume(); // -6 (4) suspended resumed. Go to coroutine.
        return Iter{m_coroutine};
    }

    std::default_sentinel_t end() { return {}; }

private:
    Handle m_coroutine;
};

template<std::integral T>
Generator<T> range(T first, const T last)
{
  // suspended right away since initial_suspend() returns std::suspend_always
  // returns to the caller with Generator instance
  std::cout << "Range\n"; // -7 from (6)
  while (first < last){
    std::cout << "Range first: " << first << "\n";
    co_yield first++; // -8, go to yield_value(), install `first` value, first is + 1, and suspend.
    // // -12, resume from (11) operator++()
    std::cout << "Range after first++: " << first << "\n";
  }
  // -14, coroutine end, suspend_always final_suspend() called.
}

int main()
{
  std::cout << "Start for loop\n"; // -1
  // Generator is destructed only once due to range loop extends lifetime.  
  for (const char i : range(65, 67))
      std::cout << i << "\n";
  // -17, out of range for loop scope, 
  // range(65, 67) returned `Generator` destructs.

  std::cout << "End for loop\n";
    std::cout << '\n';
}
stdout:
Start for loop
get_return_object()
Generator constructor
suspend_always initial_suspend()
Iter begin
Range
Range first: 65
yield_value() : 65
Iter == called: !m_coroutine: 0 m_coroutine.done(): 0
 Iter* return value: 65
A
Iter ++ currnet value: 65
Range after first++: 66
Range first: 66
yield_value() : 66
Iter ++ resumed currnet value: 66
Iter == called: !m_coroutine: 0 m_coroutine.done(): 0
 Iter* return value: 66
B
Iter ++ currnet value: 66
Range after first++: 67
suspend_always final_suspend()
Iter ++ resumed currnet value: 66
Iter == called: !m_coroutine: 0 m_coroutine.done(): 1
Generator destructor
End for loop


co_await Example:

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out) {
    std::cout << "switch_to_new_thread start\n";
    struct awaitable {
        std::jthread* p_out;
        bool await_ready() {
		  std::cout << "await ready\n";
		  return false; 
	    };
    
    	void await_suspend(std::coroutine_handle<> h) {
	    	std::cout << "await_suspend\n";
	        std::jthread& out = *p_out;
    	    if (out.joinable())
        	  throw std::runtime_error("Output jthread parameter not empty");
	        out = std::jthread([h] { 
        		std::cout << "calling handler.resume()\n";
				h.resume();
    	    });
            
        // Potential undefined behavior: accessing potentially destroyed *this
        // std::cout << "New thread ID: " << p_out->get_id() << '\n';
	        std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
    
    	void await_resume() {
	    	std::cout << "await_resume\n";
	    }
    };

    std::cout << "switch_to_new_thread about to return\n";
    
    return awaitable{&out};
}
 
struct task {
    struct promise_type {
        task get_return_object() {
			std::cout << "get_return_object()\n";
			return {}; 
		}
    
    	std::suspend_never initial_suspend() {
			std::cout << "inital_suspend()\n";
			return {}; 
		}
    
	    std::suspend_never final_suspend() noexcept {
			std::cout << "final_suspend()\n";
			return {}; 
		}
    
    	void return_void() {
			std::cout << "return_void\n";
		}
     
    	 void unhandled_exception() {}
    };

    ~task() {
	    std::cout << "task destruct\n";
    }
};
 
task resuming_on_new_thread(std::jthread& out) {
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main() {
    std::jthread out;
    std::cout << "start\n";
    resuming_on_new_thread(out);
    std::cout << "ending main()\n";
}
stdout:
start
get_return_object()
inital_suspend()
Coroutine started on thread: 140248882116480
switch_to_new_thread start
switch_to_new_thread about to return
await ready
await_suspend
New thread ID: 140248877143744
task destruct
ending main()
calling handler.resume()
await_resume
Coroutine resumed on thread: 140248877143744
return_void
final_suspend()

Apr 21, 2022

[C++][C++20] coroutine minute

Reference:
https://en.cppreference.com/w/cpp/language/coroutines
https://www.packtpub.com/product/c-high-performance-second-edition/9781839216541
https://clang.llvm.org/docs/DebuggingCoroutines.html (debugging practice / worth another post with live experience)
https://www.reddit.com/r/cpp/comments/vwt6xl/debugging_c_coroutines/

Don't use thread local inside coroutine function:
https://vsdmars.blogspot.com/2022/12/c-use-of-threadlocal-in-code.html


Restrictions



Stackless

Stackless coroutines need to store the coroutine frame somewhere else (typically on the heap) and then use the stack of the currently executing thread to store nested call frames. (i.e current stack stores nested call frames; while heap stores coroutine call frame; nested call frames are functions being called inside the coroutine frame.)

Stackless coroutines use the stack of the currently running thread to handle nested function calls.
The effect of this is that a stackless coroutine can never suspend from a nested call frame.

Memory footprint: Coroutine frame






Remember

"std::coroutine_handle<promise>" has 
"using promise_type = struct promise;" defined.
And "struct promise" provides several contract member functions and some of them returns the
"std::coroutine_handle<promise>"
promise's get_return_object() return type kind should have promise_type defined.


coroutine state

  • an internal, heap-allocated (unless the allocation is optimized out), object that contains
    • the promise object
    • the parameters (all copied by value)
    • some representation of the current suspension point, so that resume knows where to continue and destroy knows what local variables were in scope
    • local variables and temporaries whose lifetime spans the current suspension point
  • Switching between coroutines is substantially faster than switching between processes and OS threads, partly because it doesn't involve any system calls that require the CPU to run in kernel mode.
  • In general, a stackful coroutine has a more expensive context switch operation since it has more information to save and restore during suspend and resume compared to a stackless coroutine. Resuming a stackless coroutine is comparable to a normal function call.

When a coroutine begins execution, it performs the following (Important concept flow)

  • allocates the coroutine state object using operator new (see example code below)
  • copies all function parameters to the coroutine state: 
    • by-value parameters are moved or copied,
    • by-reference parameters remain references (and so may become dangling if the coroutine is resumed after the lifetime of referred object ends)
    • this pointer is also copied into the coroutine state on heap; thus beware that this is destroyed and became dangling.
  • calls the constructor for the promise object. If the promise type has a constructor that takes all coroutine parameters, that constructor is called, with post-copy coroutine arguments. Otherwise the default constructor is called.
  • calls promise.get_return_object() (the result type of get_return_object() can be any kind, this type kind is the type kind that the caller of the coroutine gets; i.e. the same type kind the coroutine function signature return type is.) and keeps the result in a local variable.
    The result of that call will be returned to the caller when the coroutine first suspends.
    Any exceptions thrown up to and including this step propagate back to the caller, not placed in the promise.
  • calls promise.initial_suspend() and co_awaits its result. Typical Promise types either return a std::suspend_always, for lazily-started coroutines; or std::suspend_never, for eagerly-started coroutines.
  • when co_await promise.initial_suspend() resumes, starts executing the body of the coroutine

When a coroutine reaches a suspension point(i.e. suspension point inside the coroutine state, i.e. co_await co_yield)

  • the return object obtained earlier(i.e. from promise.get_return_object()) is returned to the caller/resumer, after implicit conversion to the return type of the coroutine, if necessary.

When a coroutine reaches the co_return statement

  • calls promise.return_void() for
    • co_return;
    • co_return expr where expr has type void
    • falling off the end of a void-returning coroutine. The behavior is undefined if the Promise type has no Promise::return_void() member function in this case.
  • or calls promise.return_value(expr) for co_return expr where expr has non-void type
  • destroys all variables with automatic storage duration in reverse order they were created.
  • calls promise.final_suspend() and co_awaits the result.

coroutine ends with an uncaught exception

  • catches the exception and calls promise.unhandled_exception() from within the catch-block
  • calls promise.final_suspend() and co_awaits the result (e.g. to resume a continuation or publish a result). It's undefined behavior to resume a coroutine from this point.

When the coroutine state is destroyed either because it terminated via co_return or uncaught exception, or because it was destroyed via its handle

  • calls the destructor of the promise object.
  • calls the destructors of the function parameter copies.
  • calls operator delete to free the memory used by the coroutine state
  • transfers execution back to the caller/resumer.

Heap allocation

  • coroutine state is allocated on the heap via non-array operator new.
  • If the Promise type defines a class-level replacement, it will be used, otherwise global operator new will be used.
  • If the Promise type defines a placement form of operator new that takes additional parameters, and they match an argument list where the first argument is the size requested (of type std::size_t) and the rest are the coroutine function arguments, those arguments will be passed to operator new (this makes it possible to use leading-allocator-convention for coroutines)
  • The call to operator new can be optimized out (even if custom allocator is used) if
    • The lifetime of the coroutine state is strictly nested within the lifetime of the caller, and
    • the size of coroutine frame is known at the call site
    • that is to say, no escape of coroutine occurs
    • in that case, coroutine state is embedded in the caller's stack frame
      (if the caller is an ordinary function) or coroutine state (if the caller is a coroutine)
  • If allocation fails, the coroutine throws std::bad_alloc, unless the Promise type defines the member function Promise::get_return_object_on_allocation_failure()
  • If that member function is defined, allocation uses the nothrow form of operator new and on allocation failure, the coroutine immediately returns the object obtained from Promise::get_return_object_on_allocation_failure() to the caller.

Promise

The Promise type is determined by the compiler from the return type of the coroutine using std::coroutine_traits.


#include <coroutine>
#include <iostream>

// Consider promise is the config for coroutine.
struct promise;
struct coroutine : std::coroutine_handle<promise>
{ using promise_type = struct promise; };

struct promise {
  coroutine get_return_object()
  { return {coroutine::from_promise(*this)}; }
  std::suspend_always initial_suspend() noexcept { return {}; }
  std::suspend_always final_suspend() noexcept { return {}; }
  void return_void() {}
  void unhandled_exception() {}
};

struct S {
  int i;
  coroutine f() {
    std::cout << i;
    co_return;
  }
};

void bad1() {
  coroutine h = S{0}.f();
  // S{0} destroyed
  // and due to promise::initial_suspend() returns std::suspend_always ; 
  // code will suspend on previous line and only continue at h.resume()
  h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
  h.destroy();
}

coroutine bad2() {
  S s{0};
  // S is RAIIed; coroutin has the copy of pointer to s instance(i.e. this) which is now dangling.
  return s.f(); 
}

void bad3() {
  coroutine h = [i = 0]() -> coroutine { // a lambda that's also a coroutine
    std::cout << i;
    co_return;
  }(); // immediately invoked
  // lambda destroyed
  h.resume(); // uses (anonymous lambda type)::i after free
  h.destroy();
}

void good() {
  coroutine h = [](int i) -> coroutine { // make i a coroutine parameter
    std::cout << i;
    co_return;
  }(0);
  // lambda destroyed
  h.resume(); // no problem, i has been copied to the coroutine frame as a by-value parameter
  h.destroy();
}


Stackful

Stackful coroutines have a separate side stack (similar to a thread) that contains the coroutine frame and the nested call frames. 
Stackful coroutines are sometimes called fibers, and in the programming language Go, they are called goroutines. (more details in blog's goroutine notes)
Stackful coroutines remind us of threads, where each thread manages its own stack. 
There are two big differences between stackful coroutines (or fibers) and OS threads:
  • OS threads are scheduled by the kernel and switching between two threads is a kernel mode operation.
  • Most OSes switch OS threads preemptively (the thread is interrupted by the scheduler), whereas a switch between two fibers happens cooperatively. A running fiber keeps running until it passes control over to some manager that can then schedule another fiber.
Memory footprint: Coroutine frame + call stack


Suspend point


In details:
co_await: An operator that suspends the current coroutine
co_yield: Returns a value to the caller and suspends the coroutine 
co_return: Completes the execution of a coroutine and can, optionally, return a value


<coroutine> header including the following:


coroutine has the following restrictions:
  • A coroutine cannot use variadic arguments like f(const char*...)
  • A coroutine cannot return auto or a concept type: auto f()
  • A coroutine cannot be declared constexpr
  • Constructors and destructors cannot be coroutines
  • The main() function cannot be a coroutine

Coroutin State == Coroutine Frame ; which is create on heap(if is needed)

A bit more about co_await; which enables coroutine as an alternative of using in-elegant thread/future/promise API.

co_await

The unary operator co_await suspends a coroutine and returns control to the caller.
此時caller 拿到的operand type為awaitable
Its operand is an expression that either 
  1. is of a class type that defines a member operator co_await or may be passed to a non-member operator co_await, or 
  2. is convertible to such a class type by means of the current coroutine's Promise::await_transform.

co_await expr

expr is converted to an awaitable as follows
  • if expr is produced by an
    • initial suspend point, or
    • a final suspend point, or
    • a yield expression, 
    the awaitable is expr, as-is.
  • otherwise, if the current coroutine's Promise type has the member function await_transform, then the awaitable is promise.await_transform(expr)
  • otherwise, the awaitable is expr, as-is.

Then, the awaiter object is obtained, as follows
  • if overload resolution for operator co_await gives a single best overload, the awaiter is the result of that call (awaitable.operator co_await() for member overload,
    operator co_await(static_cast<Awaitable&&>(awaitable)) for the non-member overload)
  • otherwise, if overload resolution finds no operator co_await, the awaiter is awaitable, as-is
  • otherwise, if overload resolution is ambiguous, the program is ill-formed
If the expression above is a prvalue, the awaiter object is a temporary materialized from it. 
Otherwise, if the expression above is an glvalue, the awaiter object is the object to which it refers.
Then, awaiter.await_ready() is called.
(this is a short-cut to avoid the cost of suspension if it's known that the result is ready or can be completed synchronously, 也就是false代表返回caller,coroutine suspend; true代表立刻繼續執行coroutine). 

If its result, contextually-converted to bool is false then
  • The coroutine is suspended (its coroutine state is populated with local variables and current suspension point).
  • awaiter.await_suspend(handle) is called, where handle is the coroutine handle(就是有co_await的那個caller) representing the current coroutine(這裏的current coroutine就是有co_await字詞的那個caller).
    Inside that function, the suspended coroutine state is observable via that handle, and it's this function's responsibility to schedule it to resume on some executor, or to be destroyed (returning false counts as scheduling)
    • if await_suspend returns void, control is immediately returned to the caller/resumer of the current coroutine (this coroutine remains suspended), otherwise
    • if await_suspend returns bool,
      • the value true returns control to the caller/resumer of the current coroutine
      • the value false resumes the current coroutine.
    • if await_suspend returns a coroutine handle for some other coroutine, that handle is resumed (by a call to handle.resume())
      (note this may chain to eventually cause the current coroutine to resume)
    • if await_suspend throws an exception, the exception is caught, the coroutine is resumed, and the exception is immediately re-thrown
  • Finally, awaiter.await_resume() is called (whether the coroutine was suspended or not), and its result is the result of the whole co_await expr expression.
    await_resume() 回傳的object是指斷點恢復後繼續下去的code.
  • execution pattern與Python的yield一樣
  • If the coroutine was suspended in the co_await expression, and is later resumed, the resume point is immediately before the call to awaiter.await_resume().
  • Note that because the coroutine is fully suspended before entering awaiter.await_suspend(), that function is free to transfer the coroutine handle across threads, with no additional synchronization.
    For example, it can put it inside a callback, scheduled to run on a threadpool when async I/O operation completes.
    In that case, since the current coroutine may have been resumed and thus executed the awaiter object's destructor, all concurrently as await_suspend() continues its execution on the current thread, await_suspend() should treat *this as destroyed and not access it after the handle was published to other threads.
e.g.

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out) {
  struct awaitable {
    std::jthread* p_out;
    bool await_ready() { return false; } // returns false thus await_suspend will be called.

	// the coroutine is fully suspended before calling this function.
    // thus it is OK to pass the handle of that suspended coroutine to another thread.
    void await_suspend(std::coroutine_handle<> h /* controlls the corountin */) {
      std::jthread& out = *p_out;
      if (out.joinable()) // empty, not running yet while no callable function can be run
        throw std::runtime_error("Output jthread parameter not empty");
    
      // capture the coroutine's handler; beware *this will be destroyed after h.resume()
      // thus not to operate on captured *this (can make a copy of it though)
      out = std::jthread([h] {
          std::cout << "call h.resume" << std::endl << std::flush;
          h.resume(); 
          std::cout << "end h.resume" << std::endl << std::flush;
      });
      // Potential undefined behavior: accessing potentially destroyed *this
      // std::cout << "New thread ID: " << p_out->get_id() << '\n';
      std::cout << "New thread ID: " << out.get_id() << '\n'; // #3
    }
    
    // in final step this function will be called.
    // 可以回傳object; 此object將回用於coroutine內
    // e.g.
    // int return_value = co_await switch_to_new_thread(out);
    int await_resume() {return 42;} 

    ~awaitable() {
      std::cout << "awaitable destructor called" << std::endl << std::flush;
    }
  };
  return awaitable{&out};
}
 
struct task{
  struct promise_type {
    task get_return_object() { return {}; }
    std::suspend_never initial_suspend() { return {}; }
    std::suspend_never final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
  };
};
 
task resuming_on_new_thread(std::jthread& out) {
  std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n'; // #2
  co_await switch_to_new_thread(out);
  // awaiter destroyed here
  std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main() {
  std::jthread out; // empty, not running yet while no callable function can be run
  std::cout << "call resuming_on_new_thread" << std::endl << std::flush; // #1
  resuming_on_new_thread(out);
  std::cout << "end resuming_on_new_thread: " << std::this_thread::get_id() <<
   std::endl << std::flush; // #4
  // BLOCK from exit main() due to jthread's destructor call.
}
result:
call resuming_on_new_thread
Coroutine started on thread: 139837705672512
New thread ID: 139837685438208
end resuming_on_new_thread: 139837705672512
call h.resume
awaitable destructor called
Coroutine resumed on thread: 139837685438208
end h.resume


co_yield

Yield-expression returns a value to the caller and suspends the current coroutine: 
it is the common building block of resumable generator functions

co_yield expr 
co_yield braced-init-list

above code is same as:
co_await promise.yield_value(expr)

A typical generator's yield_value would store 
(copy/move or just store the address of, since the argument's lifetime crosses the suspension point inside the co_await)
its argument into the generator object and return std::suspend_always, transferring control to the caller/resumer.
#include <coroutine>
#include <exception>
#include <iostream>
 
template<typename T>
struct Generator {
   // The class name 'Generator' is our choice and 
   // it is not required for coroutine magic. 
   // Compiler recognizes coroutine by the presence of 'co_yield' keyword.
   // You can use name 'MyGenerator' (or any other name) instead
   // as long as you include nested struct promise_type 
   // with 'MyGenerator get_return_object()' method .
   //(Note:You need to adjust class constructor/destructor names too when choosing to rename class)
 
  struct promise_type;
  using handle_type = std::coroutine_handle<promise_type>;
 
  struct promise_type {// required 
    T value_;
    std::exception_ptr exception_;
 
    Generator get_return_object() {
      return Generator(handle_type::from_promise(*this));
    }
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() { exception_ = std::current_exception(); }//saving exception
    template<std::convertible_to<T> From> // C++20 concept
    std::suspend_always yield_value(From &&from) {
      value_ = std::forward<From>(from);//caching the result in promise
      return {};
    }
    void return_void() {}
  };
 
  handle_type h_;
 
  Generator(handle_type h) : h_(h) {}
  ~Generator() { h_.destroy(); }
  explicit operator bool() {
    fill();// The only way to reliably find out whether or not we finished coroutine, 
           // whether or not there is going to be a next value generated (co_yield) in coroutine
           // via C++ getter (operator () below) 
           // is to execute/resume coroutine until the next co_yield point (or let it fall off end).
           // Then we store/cache result in promise to allow getter (operator() below to grab it 
           // without executing coroutine)
    return !h_.done();
  }
  T operator()() {
    fill();
    full_ = false;//we are going to move out previously cached result to make promise empty again
    return std::move(h_.promise().value_);
  }
 
private:
  bool full_ = false;
 
  void fill() {
    if (!full_) {
      h_();
      if (h_.promise().exception_)
        std::rethrow_exception(h_.promise().exception_);
        //propagate coroutine exception in called context
 
      full_ = true;
    }
  }
};
 
Generator<uint64_t>
fibonacci_sequence(unsigned n)
{
 
  if (n==0)
    co_return;
 
  if (n>94)
    throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
 
  co_yield 0;
 
  if (n==1)
    co_return;
 
  co_yield 1;
 
  if (n==2)
    co_return;
 
  uint64_t a=0;
  uint64_t b=1;
 
  for (unsigned i = 2; i < n;i++)
  {
    uint64_t s=a+b;
    co_yield s;
    a=b;
    b=s;
  }
}
 
int main()
{
  try {
 
    auto gen = fibonacci_sequence(10); //max 94 before uint64_t overflows
 
    for (int j=0;gen;j++)
      std::cout << "fib("<<j <<")=" << gen() << '\n';
 
  }
  catch (const std::exception& ex)
  {
    std::cerr << "Exception: " << ex.what() << '\n';
  }
  catch (...)
  {
    std::cerr << "Unknown exception.\n";
  }
}

Jan 27, 2019

[split stack] reading notes and references

Reference:
gccgo split stack implementation
  1. The stack can start splitting at any point.
  2. The stack size is automatically recorded at program startup,
    and each thread startup.
  3. The gold linker detects calls from split-stack code to non-split-stack
    code, and rewrites the function header to force a large stack segment to be allocated.
    i.e.
    When not using the gold linker, calls from split-stack code to non-split-stack code will just have whatever is left of the current stack segment, which may not be large enough.
    (look up to "Backward compatibility" section)


In the complex GCC ecosystem the linker is separate from the compiler.
GCC can't assume that gold is available at all.
When building gccgo, configure using
--with-ld=/path/to/gold

The -fuse-ld=gold option is newer than gccgo.
Ian supposes it would be nice if:
* the GCC configure process checks whether -fuse-ld=gold works; if so:
  * -fuse-ld=gold is passed to the libgo configure/build
  * -fuse-ld=gold is used by default by the gccgo driver program



Reference:
Split Stacks in GCC




Obvious benefits

  • The memory usage of a typical multi-threaded program can decrease significantly, as each thread does not require a worst-case stack size.
  • It becomes possible to run millions of threads
    (either full NPTL threads or co-routines) in a 32-bit address space.




Basic explained

Stack will have a guaranteed zone which is always available.
Reference:
[LWN] Preventing stack guard-page hopping


The size of the guard area will be target specific.
It will include enough stack space to actually allocate more stack space.
Each function will have to verify that it has enough space in the current stack to execute.

The basic verification will be a comparison between the stack pointer and the current bottom of the stack plus the guaranteed zone size.
This will have to be the first operation in the function, and will also be target specific.

It must be fast, as it will be executed by each called function.

Two cases to consider.
  1. For functions which require a stack frame less than the size of the guaranteed guard area, we can do a simple comparison between the stack pointer and the stack limit.
  2. For functions which require a larger stack frame, we must do a comparison including the size of the stack frame.




Design options

  1. Reserve a register to hold the bottom of the stack plus the guaranteed size. This will have to be a callee-saved register.
  2. Use a TLS(Thread Local Storage) variable. In the general case, in a shared library, this will require calling the __tls_get_addr function.
    Reference:
    How fast is thread local variable access on Linux
    (GOLD elf linker)
    http://gittup.org/cgi-bin/man/man2html?gold+1 

    That means that that function will have to work without requiring any additional stack space.
    This is infeasible unless the whole system is compiled with split stacks.
    It would require dlopen's LD_BIND_NOW to be set, so that the __tls_get_addr function is resolved at program startup time.
    Even that is probably insufficient unless we can ensure that the space for the (TLS) variable is fully allocated.
    In general Ian doesn't think they can ensure this, because dlopen can cause a thread to require more space for TLS variables, and that space will be allocated on the first call to __tls_get_addr.
    Reference:
    http://man7.org/linux/man-pages/man8/ld.so.8.html
    LD_BIND_NOW (since glibc 2.1.1)
    If set to a nonempty string, causes the dynamic linker to
    resolve all symbols at program startup instead of deferring
    function call resolution to the point when they are first
    referenced.  This is useful when using a debugger.
  3. Have the stack always end at a N-bit boundary.
    E.g., if we always allocate stack segments as a multiple of 4K,
    then align each one so that the stack always ends at a 12-bit boundary.
    Then the amount of space remaining on the stack is SP & 0xfff.
  4. Introduce a new function call which handles the comparison of the stack pointer and the stack expansion.
  5. Reuse the stack protector support field.
    When using glibc each thread descriptor has a field used by the stack protector.
    Of course it is then not possible to use split stacks in conjunction with stack protector.
  6. At least on x86, arrange to allocate a new field in the TCB(thread control block) header accessible via %fs or %gs.
    This is probably the best solution, and it is the one implemented for i386 and x86_64.

Reference:
TCB Thread Control Block in linux kernel:
https://en.wikipedia.org/wiki/Thread_control_block



Expanding the stack

  • Expanding the stack requires allocating additional memory.
  • This additional memory will have to be allocated using only the stack space slot.
  • All of the functions used to allocate additional stack space must be compiled to not use a split stack.
  • A new function attribute, no_split_stack will be introduced to mean that the stack should not be split.
  • It would also work to ensure that the stack is large enough that they do not need to split the stack during the allocation call.
  • After expanding the stack, the function will copy any stack based parameters from the old stack to the new stack.
  • Fortunately, all C++ objects which require a copy or move constructor are implicitly passed by reference,so copying the parameters on the stack is OK.
  • For varargs functions, this is impossible in general, so we will compile varargs functions differently:
    they will use an argument pointer which is not necessarily based on the frame pointer.
    For functions which return objects on the stack, the objects will be returned on the old stack. (RVO)
    This should normally happen automatically, as the initial hidden parameter will naturally point to the old stack.
  • When expanding the stack, the return address of the function will be managed to point to a function which will release the allocated stack block and reset the stack pointer to the caller.
    Reference:
    http://vsdmars.blogspot.com/2017/11/assembly-note.html
  • The address of the old stack block, and the old stack pointer, will have been saved somewhere in the new stack block.




Backward compatibility

We want to be able to use split stack programs on systems with pre-built libraries compiled without split stacks.
This means that we need to ensure that there is sufficient stack space before calling any such function.

Each object file compiled in split stack mode will be annotated to indicate that the functions use split stacks.

This should probably be annotated with a note but there is no general support for creating arbitrary notes in GNU as.

Therefore, each object file compiled in split stack mode will have an empty section with a special name: .note.GNU-split-stack

If an object file compiled in split stack mode includes some functions with the no_split_stack attribute, then the object file will also have a .note.GNU-no-split-stack section.

This will tell the linker that some functions may not have the expected split stack prologue.

When the linker links an executable or shared library, it will look for calls from split-stack code to non-split-stack code.

This will include calls to non-split-stack shared libraries
(thus, a program linked against a split-stack shared library may fail if at runtime the dynamic linker finds a non-split-stack shared library;
it might be desirable to use a new segment type to detect this situation).

For calls from split-stack code to non-split-stack code, the linker will change the initial instructions in the split-stack (caller) function.
This means that the linker will have to have special knowledge of the instructions that the compiler emits.
The effect of the changes will be to increase the required frame-size by a number large enough to reasonably work for a non-split-stack.
This will be a target dependent number; the default will be something like 64K.
Note that this large stack will be released when the split-stack function returns.
Note that I'm disregarding the case of split-stack code in a shared library calling non-split-stack code in the main executable; that seems like an unlikely problem.


Function pointers are a tricky case.
In general we don't know whether a function pointer points to split-stack code.
Therefore, all calls through a function pointer will be modified to call (or jump to) a special function __fnptr_morestack.
This will use a target specific function calling sequence, and will be implemented as though it were itself a function call instruction.
That is, all the parameters will be set up, and then the code will jump to __fnptr_morestack.
The __fnptr_morestack function takes two parameters: the function pointer to call, and the number of bytes of arguments pushed on the stack.