Introduction to C++20 Coroutines - Part 5 stackful/stackless and Symmetric Transfer
This series is related to C++20 coroutine.
part 1: Generator
part 2: co_await/co_return
part 3: Some tips about life cycle when using coroutines
part 4: Some examples about co_await
part 5: stackful/stackless and Symmetric Transfer
stackful/stackless
Coroutines can be divided into stackful coroutines (such as goroutine in Golang) and stackless coroutines (such as async
/await
in JavaScript).
What the called stackful and stackless stands for is not whether needing a stack or not when coroutines run. As we all know, coroutines can’t run without a stack space. It means whether coroutines can be suspended in their any nested functions.
For details, please refer to this blog https://mthli.xyz/stackful-stackless. I think it’s clear.
Symmetric Transfer
Crash Case
This case in from this blog https://lewissbaker.github.io/2020/05/11/understanding_symmetric_transfer that is written by the author of the library cppcoro
.
I modified the case a little to let it run in function main
.
1 |
|
In my machine, it works with loop_synchronously(100000)
while it can’t with loop_synchronously(1000000)
. It will crash due to segmentation fault, and its call stack is very very deep when viewed in GDB. It’s because this kind of symmetric transfer is called by h.promise().continuation.resume()
and coro_.resume()
bidirectionally, and the call stack only becomes deeper and never releases. In this way, stack overflow will happen sooner or later as the size of loop is growing. The details of the call stack and the analysis of the crash can be found refer to the above mentioned blog.
Solution
Change the
task::awaiter::await_suspend
method from this:1
2
3
4
5
6
7
8
9void task::awaiter::await_suspend(std::coroutine_handle<> continuation) noexcept {
// Store the continuation in the task's promise so that the final_suspend()
// knows to resume this coroutine when the task completes.
coro_.promise().continuation = continuation;
// Then we resume the task's coroutine, which is currently suspended
// at the initial-suspend-point (ie. at the open curly brace).
coro_.resume();
}to this:
1
2
3
4
5
6
7
8
9
10std::coroutine_handle<> task::awaiter::await_suspend(std::coroutine_handle<> continuation) noexcept {
// Store the continuation in the task's promise so that the final_suspend()
// knows to resume this coroutine when the task completes.
coro_.promise().continuation = continuation;
// Then we tail-resume the task's coroutine, which is currently suspended
// at the initial-suspend-point (ie. at the open curly brace), by returning
// its handle from await_suspend().
return coro_;
}Update the
task::promise_type::final_awaiter::await_suspend
method from this:1
2
3
4
5
6
7void task::promise_type::final_awaiter::await_suspend(std::coroutine_handle<promise_type> h) noexcept {
// The coroutine is now suspended at the final-suspend point.
// Lookup its continuation in the promise and resume it.
if (h.promise().continuation) {
h.promise().continuation.resume();
}
}to this:
1
2
3
4
5std::coroutine_handle<> task::promise_type::final_awaiter::await_suspend(std::coroutine_handle<promise_type> h) noexcept {
// The coroutine is now suspended at the final-suspend point.
// Lookup its continuation in the promise and resume it symmetrically.
return (h.promise().continuation) ? h.promise().continuation : std::
}
In this way, stack overflow would never happen.