Skip to content

C++20 Coroutines — co_await·co_yield·co_return 내부 메커니즘

C++20 Coroutines는 일시 중단(suspend)과 재개(resume)가 가능한 함수를 언어 차원에서 지원합니다. 함수가 중간에 멈추고, 나중에 멈춘 지점부터 다시 실행할 수 있어 Generator, 비동기 I/O, 상태 머신 구현에 매우 적합합니다.

코루틴 함수의 특징:

  • 함수 본문에 co_await, co_yield, co_return 중 하나 이상 포함
  • 반환 타입은 반드시 Promise 타입 프로토콜을 구현해야 함
  • 호출 시 즉시 실행되지 않고 코루틴 프레임(Coroutine Frame)이 힙에 할당됨

키워드역할
co_await exprexpr의 Awaitable이 ready가 아니면 코루틴 일시 중단
co_yield value값을 호출자에게 전달하고 일시 중단
co_return value코루틴 최종 완료, 반환값 설정
// co_yield 사용 예 — Generator 스타일
Generator<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
co_yield a; // a를 호출자에게 전달하고 중단
int tmp = a + b;
a = b;
b = tmp;
}
}
// co_await 사용 예 — 비동기 태스크
Task<std::string> FetchData(std::string url)
{
auto response = co_await HttpGet(url); // 완료될 때까지 중단
auto parsed = co_await ParseJson(response);
co_return parsed["name"]; // 최종값 반환
}

2. 코루틴 내부 구조 — Promise 타입

Section titled “2. 코루틴 내부 구조 — Promise 타입”

모든 코루틴은 반환 타입에 연결된 promise_type 이너 클래스를 통해 동작합니다.

#include <coroutine>
// 간단한 Generator 구현
template<typename T>
struct Generator
{
// 필수 inner 타입: promise_type
struct promise_type
{
T current_value;
// 코루틴 객체 생성
Generator get_return_object()
{
return Generator{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
// 코루틴 시작 시 즉시 중단? (suspend_always = true)
std::suspend_always initial_suspend() { return {}; }
// 코루틴 완료 후 중단?
std::suspend_always final_suspend() noexcept { return {}; }
// co_yield 처리
std::suspend_always yield_value(T value)
{
current_value = value;
return {}; // 항상 중단
}
// co_return; (void)
void return_void() {}
// 예외 처리
void unhandled_exception() { std::terminate(); }
};
// 코루틴 핸들 — 재개/소멸 제어
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator()
{
if (handle) handle.destroy();
}
// 다음 값 가져오기
bool MoveNext()
{
handle.resume();
return !handle.done();
}
T Current() const { return handle.promise().current_value; }
};

Generator<int> Range(int start, int end, int step = 1)
{
for (int i = start; i < end; i += step)
co_yield i;
}
Generator<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
co_yield a;
auto tmp = a + b;
a = b;
b = tmp;
}
}
// 사용
auto gen = Range(0, 10, 2);
while (gen.MoveNext())
std::cout << gen.Current() << " "; // 0 2 4 6 8
auto fib = Fibonacci();
for (int i = 0; i < 10 && fib.MoveNext(); ++i)
std::cout << fib.Current() << " "; // 0 1 1 2 3 5 8 13 21 34

co_await expr를 만나면 컴파일러는 expr의 Awaitable 프로토콜을 호출합니다.

// Awaitable이 되기 위한 세 가지 멤버 함수
struct MyAwaitable
{
// 이미 완료됐는가? true면 중단 없이 즉시 계속 실행
bool await_ready() const noexcept { return false; }
// 중단 직전 호출 — 재개 로직(콜백 등록 등) 수행
// handle: 현재 코루틴의 핸들 — 재개하려면 handle.resume() 호출
void await_suspend(std::coroutine_handle<> handle)
{
// 예: 비동기 I/O 완료 콜백에서 handle.resume() 예약
some_async_operation([handle]() mutable { handle.resume(); });
}
// 재개 후 co_await 표현식의 최종 결과값
int await_resume() const noexcept { return 42; }
};
// 사용
Task<void> Example()
{
int result = co_await MyAwaitable{}; // result = 42
}
타입의미
std::suspend_always항상 중단 (await_ready() = false)
std::suspend_never절대 중단 안 함 (await_ready() = true)

5. Task — 비동기 코루틴 반환 타입

Section titled “5. Task — 비동기 코루틴 반환 타입”
#include <coroutine>
#include <exception>
// 단순 Task<T> 구현 (실제 프로덕션은 더 복잡)
template<typename T>
struct Task
{
struct promise_type
{
T result;
std::exception_ptr exception;
Task get_return_object()
{
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; } // 즉시 실행
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T value) { result = std::move(value); }
void unhandled_exception()
{
exception = std::current_exception();
}
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
T Get()
{
if (handle.promise().exception)
std::rethrow_exception(handle.promise().exception);
return handle.promise().result;
}
};

6. 코루틴 실행 흐름 다이어그램

Section titled “6. 코루틴 실행 흐름 다이어그램”
호출자 코루틴 함수 본문
| |
|--- 코루틴 생성 ------------>|
| initial_suspend()
|<-- 제어권 반환 -------------|
|
|--- handle.resume() -------->|
| [실행]
| co_yield v
|<-- 값 전달 + 중단 ----------|
|
|--- handle.resume() -------->|
| [재개]
| co_return result
| final_suspend()
|<-- 완료 -------------------|
|
|--- handle.destroy() ------->| (소멸자에서 자동 호출)

코루틴이 처음 호출되면 컴파일러는 다음 정보를 힙에 할당합니다:

// 컴파일러가 내부적으로 생성하는 코루틴 프레임 (의사 코드)
struct CoroutineFrame
{
// Promise 객체
promise_type promise;
// 현재 중단 포인트 (재개 위치를 결정하는 상태 변수)
int suspend_index;
// 코루틴 본문에서 사용된 지역 변수들
T local_var1;
U local_var2;
// ...
// resume/destroy 함수 포인터
void (*resume_fn)(CoroutineFrame*);
void (*destroy_fn)(CoroutineFrame*);
};

이 때문에 코루틴은 힙 할당 비용이 있으며, 컴파일러 최적화(Heap Allocation Elision)가 적용될 경우 스택으로 대체되기도 합니다.


Generator<std::string> StateMachine()
{
co_yield "초기화 중...";
co_yield "데이터 로딩 중...";
co_yield "처리 중...";
co_yield "완료";
}
auto sm = StateMachine();
while (sm.MoveNext())
{
std::cout << sm.Current() << "\n";
// 각 상태에서 UI 업데이트, 이벤트 처리 등 수행
}
struct TreeNode { int value; TreeNode* left; TreeNode* right; };
Generator<int> InOrder(TreeNode* node)
{
if (!node) co_return;
// 재귀 호출 대신 co_yield from 패턴 (직접 지원 없어 수동 체이닝)
auto left = InOrder(node->left);
while (left.MoveNext()) co_yield left.Current();
co_yield node->value;
auto right = InOrder(node->right);
while (right.MoveNext()) co_yield right.Current();
}

항목내용
힙 할당코루틴 프레임은 기본적으로 힙에 할당됨
수명 관리코루틴 핸들 미소멸 시 메모리 누수
표준 라이브러리 부재C++20에는 Task, Generator 표준 타입 없음 (C++23에 std::generator 추가)
예외 전파unhandled_exception()을 반드시 구현해야 함
스레드 안전코루틴 자체는 스레드 안전 보장 없음 — 동기화 직접 구현 필요

#include <generator> // C++23
std::generator<int> Fib()
{
auto [a, b] = std::pair{0, 1};
while (true)
{
co_yield a;
std::tie(a, b) = std::pair{b, a + b};
}
}
// Ranges와 완벽 통합
for (int n : Fib() | std::views::take(10))
std::cout << n << " ";

구분내용
co_yield값 전달 + 중단 — Generator 패턴
co_await비동기 대기 — Task/Future 패턴
co_return최종 값 반환 및 코루틴 완료
promise_type코루틴 동작 정의 — 직접 구현 또는 라이브러리 사용
Awaitableawait_ready/suspend/resume 세 함수 구현

C++20 Coroutines는 강력하지만 로우레벨 인터페이스입니다. 실무에서는 cppcoro, Asio, Folly와 같은 라이브러리가 제공하는 Task, Generator 타입을 사용하거나, C++23 std::generator를 활용하는 것이 권장됩니다.