Coroutines: An Overview¶
Foreword¶
A “function” that is not a function
Entered multiple times (what?)
⟶ Suspended and resumed
“Stackless” (whatever that means)
Use case: Async
Looks like blocking, but isn’t
Event loop, but without callbacks
Multithreading replacement
Much like Python’s
asyncio
Use case: generators (since C++23)
Prototypical Introductory Exampe: Fibonacci Numbers¶
Focus on usage
Coroutine definition
Fibonacci fibonacci() { co_yield ...; }
Coroutine instantiation
auto fibo = fibonacci();
Coroutine usage ⟶ Range-based-for on a generator?
Step By Step: Simplest¶
What I want is …
Coro hello()
{
std::cout << "Hello" << std::endl;
co_return;
}
int main()
{
auto hello_instance = hello();
return 0;
}
Simplest: Incremental Fixing And Explaining¶
promise_type
: expected by the compiler
#include <coroutine>
struct Coro {
struct promise_type
{
Coro get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept(true) { return {}; }
void return_void() {}
void unhandled_exception() {}
};
};
get_return_object()
: inserted be compiler when coroutine is instantiated (hello()
)initial_suspend()
: don’t execute any code before somebody callsresume()
final_suspend()
: don’t execute any code after falling off the endvoid return_void()
: apparently that is another customization pointvoid unhandled_exception()
: ideally an exception should be propagated to the caller (we ignore it)
Try it out
Nothing happens ⟶ “Hello” not printed
Replace
initial_suspend()
return type tostd::suspend_never
⟶ “Hello” printed
Background: customization
<coroutine>
is a set of building blocksAsync
Generators
…
Driving Coroutines: Coroutine Anatomy¶
auto hello_instance = hello();
Coroutine object created magically (
Coro
is only a wrapper type)Must be stored somewhere
Associated with a
promise_type
objectCoroutine type:
std::coroutine_handle<promise_type>
Retrieving the coroutine object by
promise_type
:std::coroutine_handle<promise_type>::from_promise(promise_type&)
Customization point:
get_return_object()
Driving Coroutines: Resuming¶
Transform
Coro
into a classAdd
Coro
member:std::coroutine_handle<promise_type> _coro
Coro::resume()
Call in
main()
⟶ resumed until
co_return
#include <coroutine>
#include <iostream>
class Coro {
public:
struct promise_type
{
Coro get_return_object() { return Coro(this); }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept(true) { return {}; }
void return_void() {}
void unhandled_exception() {}
};
using Handle = std::coroutine_handle<promise_type>;
public:
Coro(promise_type* p) : _coro(Handle::from_promise(*p)) {}
void resume() { _coro.resume(); }
private:
Handle _coro;
};
Coro hello()
{
std::cout << "Hello" << std::endl;
co_return;
}
int main()
{
auto hello_instance = hello();
hello_instance.resume();
return 0;
}
Suspension: Returning Control To Caller (co_yield
)¶
co_yield
: returns control to coroutine caller.resume()
: re-enters coroutine - this is the definition of coroutines
struct promise_type
{
std::suspend_always yield_value(std::string);
}
Coro hello()
{
std::cout << "Saying Hello" << std::endl;
co_yield "Hello";
std::cout << "Not Saying Bye" << std::endl;
co_return;
}
int main()
{
auto hello_instance = hello();
hello_instance.resume(); // <--- yields into promise
auto value = hello_instance.last_value(); // <--- get yielded value from promise
std::cout << "coro produced: " << value << std::endl;
hello_instance.resume(); // <--- terminate: resume until co_return
return 0;
}
Customization point:
yield_value()
co_yield
parameter ideally stored in promise objectMade available though wrapper class
Coro
Playing Around: Iteration, Mimicking Python Iterator Protocol¶
Coro::StopIteration
Coro::next()
Iteration …
while (true) { try { std::cout << hello_instance.next() << std::endl; } catch (const Coro::StopIteration&) { break; } }
Playing Around: Iteration, Range-Based-For¶
Coro
’s own iteratorstruct sentinel {}; struct iterator { std::coroutine_handle<promise_type> coro; bool operator==(sentinel) const { return coro.done(); } iterator& operator++() { coro.resume(); return *this; } std::string operator*() const { return coro.promise().last_value; } }; iterator begin() const { return {std::coroutine_handle<promise_type>::from_promise(*_promise)}; } sentinel end() const { return {}; }
Iteration …
for (auto elem: hello_instance) std::cout << elem << std::endl;
Bug fix:
std::suspend_never()
must have return typestd::suspend_never
!
Playing Around: Generic Generator¶
Future of C++: more tooling
Writing all that coroutine glue is not for beginners
⟶
Generator<T>
(Simply replace
Coro
with a template)A future C++ version will deduce coroutine type to just that
Generator<T>
(or similar)
Playing Around: Fibonacci Numbers, Generator Version¶
Using
Generator<T>
for fibonacci coroutine (see beginning)
Pitfalls: Coroutines Are Stateful!¶
Debugging is close to impossible
⟶ Get it right from the beginning
Pitfall (only one of many, I’m certain):
Don’t access coroutine parameters by reference!
Broken version
#include "generator.h"
#include <iostream>
#include <vector>
Generator<int> cycle(const std::vector<int>& elems)
{
while (true)
for (const auto& e: elems) // <--- BOOM!!
co_yield e;
}
int main()
{
auto c = cycle({1,2,3,4}); // <--- temporary
// <--- temporary gone here
for (auto elem: c) // <--- c still has reference to it
std::cout << elem << std::endl;
return 0;
}
Fixed version
(Move is ok too, clearly)
#include "generator.h"
#include <iostream>
#include <vector>
Generator<int> cycle(const std::vector<int> elems) // <--- BY COPY!!
{
while (true)
for (const auto& e: elems)
co_yield e;
}
int main()
{
auto c = cycle({1,2,3,4});
for (auto elem: c)
std::cout << elem << std::endl;
return 0;
}