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
- 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(). - Control the suspend from mem_fn that it's signature returns awaitable type.
- Store {yeild values/caller's handler} inside the promise instance.
- 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() - 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. - Thread local variables should not be used in coroutines to prevent buggy logic.
https://rules.sonarsource.com/cpp/RSPEC-6367
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
*/
}
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.