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 (
Corois only a wrapper type)Must be stored somewhere
Associated with a
promise_typeobjectCoroutine 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
Corointo a classAdd
Coromember:std::coroutine_handle<promise_type> _coroCoro::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_yieldparameter ideally stored in promise objectMade available though wrapper class
Coro
Playing Around: Iteration, Mimicking Python Iterator Protocol#
Coro::StopIterationCoro::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
Corowith 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;
}