C++ Coroutines | Asynchronous Programming in C++20
이 글의 핵심
Overview of C++ coroutines: promise types, suspension, and practical patterns with code.
Coroutine basics
#include <coroutine>
#include <iostream>
using namespace std;
struct Task {
struct promise_type {
Task get_return_object() {
return Task{handle_type::from_promise(*this)};
}
suspend_never initial_suspend() { return {}; }
suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
};
using handle_type = coroutine_handle<promise_type>;
handle_type coro;
Task(handle_type h) : coro(h) {}
~Task() { if (coro) coro.destroy(); }
};
Task simpleCoroutine() {
cout << "Coroutine start" << endl;
co_return;
}
int main() {
simpleCoroutine();
}
Generators
#include <coroutine>
#include <iostream>
using namespace std;
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{handle_type::from_promise(*this)};
}
suspend_always initial_suspend() { return {}; }
suspend_always final_suspend() noexcept { return {}; }
suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() {}
};
using handle_type = coroutine_handle<promise_type>;
handle_type coro;
Generator(handle_type h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
bool move_next() {
coro.resume();
return !coro.done();
}
T current_value() {
return coro.promise().current_value;
}
};
Generator<int> counter(int max) {
for (int i = 0; i < max; i++) {
co_yield i;
}
}
int main() {
auto gen = counter(5);
while (gen.move_next()) {
cout << gen.current_value() << " "; // 0 1 2 3 4
}
}
co_await
struct Awaitable {
bool await_ready() { return false; }
void await_suspend(coroutine_handle<>) {}
void await_resume() {}
};
Task asyncFunction() {
cout << "Start" << endl;
co_await Awaitable{};
cout << "Resumed" << endl;
}
Practical examples
Example 1: Fibonacci generator
Generator<int> fibonacci(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; i++) {
co_yield a;
int temp = a;
a = b;
b = temp + b;
}
}
int main() {
auto fib = fibonacci(10);
while (fib.move_next()) {
cout << fib.current_value() << " ";
}
}
Example 2: Range generator
Generator<int> range(int start, int end, int step = 1) {
for (int i = start; i < end; i += step) {
co_yield i;
}
}
int main() {
for (auto gen = range(0, 10, 2); gen.move_next();) {
cout << gen.current_value() << " "; // 0 2 4 6 8
}
}
Example 3: Read file lines
#include <fstream>
Generator<string> readLines(const string& filename) {
ifstream file(filename);
string line;
while (getline(file, line)) {
co_yield line;
}
}
int main() {
auto lines = readLines("input.txt");
while (lines.move_next()) {
cout << lines.current_value() << endl;
}
}
Example 4: Async timer
#include <chrono>
#include <thread>
struct Timer {
chrono::milliseconds duration;
bool await_ready() { return false; }
void await_suspend(coroutine_handle<> h) {
thread([h, d = duration]() {
this_thread::sleep_for(d);
h.resume();
}).detach();
}
void await_resume() {}
};
Task asyncTask() {
cout << "Start" << endl;
co_await Timer{chrono::seconds(1)};
cout << "After 1 second" << endl;
co_await Timer{chrono::seconds(1)};
cout << "After 2 seconds total" << endl;
}
Coroutines vs threads
// Thread (heavier)
void threadExample() {
thread t([] {
// work
});
t.join();
}
// Coroutine (lighter)
Task coroutineExample() {
co_await someOperation();
// work
}
Differences:
- Thread: OS resource, context-switch cost
- Coroutine: user space, lightweight suspend
Common problems
Problem 1: Missing promise_type
// Error without promise_type
Task myCoroutine() {
co_return;
}
// Define promise_type inside Task
struct Task {
struct promise_type {
// ...
};
};
Problem 2: Forgetting to destroy the handle
// Leak if destructor omits destroy
Generator<int> gen = counter(10);
// Destructor should call coro.destroy()
~Generator() {
if (coro) coro.destroy();
}
Problem 3: Local variable lifetime
Task bodies store locals in the coroutine frame; still avoid dangling references to outer stack objects across suspension points without copying.
Coroutine state
coroutine_handle<> h = ...;
h.resume();
h.done();
h.destroy();
h.promise();
FAQ
Q1: When should I use coroutines?
A: Async I/O, generators, state machines, cooperative multitasking.
Q2: Coroutines vs async/await in other languages?
A: C++ coroutines are low-level; libraries like cppcoro add ergonomics.
Q3: Performance?
A: Much lighter than OS threads; many concurrent coroutines are feasible.
Q4: Compiler support?
A: GCC 10+, Clang 14+, MSVC 2019+.
Q5: Debugging?
A: Harder than plain functions; add logging; debugger support is limited.
Q6: Learning resources?
A: cppreference.com, “C++20: The Complete Guide”, cppcoro, Lewis Baker blog.
Related reading
- C++ future and promise
- C++20 Coroutines guide
- C++20 Coroutines (English)
Practical tips
Debugging
- Check compiler warnings first
- Reproduce with a small test case
Performance
- Profile before optimizing
Code review
- Follow team conventions
Production checklist
Before coding
- Best fit for the problem?
- Maintainable by the team?
- Meets performance needs?
While coding
- Warnings cleared?
- Edge cases handled?
At review
- Intent clear?
- Tests sufficient?
Keywords
C++, coroutine, C++20, async, asynchronous
Related posts
- C++ series
- C++ Adapter Pattern
- C++ ADL
- C++ aggregate initialization