C++20이 도입되면서 가장 주목받은 기능 중 하나는 코루틴이다. 코루틴은 기존 함수 호출과 달리 실행을 중단하고 다시 이어갈 수 있다는 점에서 새로운 프로그래밍 패러다임을 가능하게 한다. 특히 비동기 처리를 표현할 때 강력한 도구가 된다. 그 핵심에 자리 잡은 개념이 바로 awaitable과 task다.

우선 task는 하나의 비동기 작업 단위를 표현하는 객체다. 함수가 값을 즉시 반환하지 않고, 미래에 준비될 값을 약속하는 컨테이너와 같다. 이 task는 단순히 비동기적으로 실행되는 함수의 껍질이 아니라, 그 함수의 실행 상태를 추적하고, 필요할 때 결과를 기다릴 수 있도록 만들어진다. 따라서 task는 단순한 함수 호출과 달리, 현재 실행 중인 루틴과 독립적으로 진행되며, 호출자는 그 결과를 co_await을 통해 기다릴 수 있다.
awaitable은 이러한 task가 co_await 키워드와 함께 동작할 수 있도록 보장하는 인터페이스다. C++에서는 awaitable을 만들기 위해 반드시 세 가지 함수를 정의해야 한다. await_ready, await_suspend, await_resume이 그것이다.
await_ready는 이 작업이 즉시 완료 가능한지 여부를 알려주고, await_suspend는 아직 준비되지 않은 경우 현재 코루틴을 중단시키며, 준비가 되면 다시 이어줄 책임을 진다. 마지막으로 await_resume은 실제 결과 값을 돌려주는 역할을 한다. 이 세 가지 함수가 합쳐져야만, 코루틴이 단순히 멈췄다가 다시 이어지는 것이 아니라, 논리적으로 일관된 비동기 흐름을 만들어낼 수 있다.
Task와 promise_type
코루틴 함수가 호출되면, 컴파일러는 내부적으로 promise_type이라는 객체를 생성한다. 이 객체는 코루틴 실행에 필요한 상태를 담고 있으며, 코루틴과 호출자 사이의 다리 역할을 한다.
여기서 중요한 두 가지 함수가 initial_suspend와 final_suspend다.
initial_suspend은 코루틴이 처음 시작될 때 바로 실행을 이어갈지, 아니면 호출자에게 제어를 돌려줄지를 결정한다. 보통std::suspend_always를 반환하여 처음에는 멈추고, 호출자가 명시적으로 실행을 이어가도록 한다.final_suspend은 코루틴이 종료된 뒤에도 잠시 멈춤 상태로 남아 있게 한다. 이렇게 해야 코루틴이 완전히 파괴되기 전에, 다른 코루틴이나 awaitable이 그 종료를 인지하고 안전하게 정리할 수 있다. 이 역시std::suspend_always가 흔히 사용된다.
Delay 기반 예제
다음은 일정 시간 지연을 비동기 task로 구현한 예제다. 이 코드는 한 쓰레드 안에서 main 함수는 진행되면서도, 코루틴 내부에서는 시간을 기다렸다가 완료되면 다시 이어서 실행된다.
#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>
// 간단한 task 정의
struct Task {
struct promise_type {
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
Task(std::coroutine_handle<promise_type> h) : handle(h) {}
~Task() { if (handle) handle.destroy(); }
};
// awaitable: delay 구현
struct Delay {
std::chrono::milliseconds duration;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
std::thread([h, d=duration]() {
std::this_thread::sleep_for(d);
h.resume();
}).detach();
}
void await_resume() const noexcept {}
};
// 비동기 함수
Task delayed_message() {
std::cout << "3초 대기 중...\n";
co_await Delay{std::chrono::seconds(3)};
std::cout << "대기 완료!\n";
}
// 실행
int main() {
auto task = delayed_message();
task.handle.resume(); // 코루틴 시작
std::cout << "메인 함수는 계속 실행 중...\n";
std::this_thread::sleep_for(std::chrono::seconds(5));
}
이 코드를 실행하면 main 함수는 즉시 "메인 함수는 계속 실행 중..."을 출력한다. 동시에 코루틴은 "3초 대기 중..."을 출력하고, 백그라운드에서 3초 대기 후 "대기 완료!"를 출력한다. 중요한 점은 main 함수가 코루틴의 지연과 무관하게 계속 진행된다는 것이다.
co_await 내부 과정

마지막으로 co_await이 호출될 때 내부적으로 어떤 일이 벌어지는지 살펴보자.
- 컴파일러는 awaitable 객체의
await_ready를 호출한다. 준비가 되었으면 바로await_resume으로 넘어간다. - 준비되지 않았다면
await_suspend가 호출된다. 이 시점에서 현재 코루틴은 중단되고 제어는 호출자에게 돌아간다. - awaitable이 준비되면, 다시 중단된 지점으로 돌아와
await_resume을 실행하면서 값을 돌려준다.
이 흐름을 통해 co_await은 단순한 문법적 장식이 아니라, awaitable과 promise_type, 코루틴 실행 컨텍스트가 정교하게 맞물려 동작하는 결과임을 알 수 있다.
결국 awaitable과 task는 C++20 코루틴이 제공하는 실질적인 가치를 보여주는 핵심 도구다. 단순한 delay 예제에서 출발했지만, 이 개념은 네트워크 IO나 파일 시스템 작업, 더 나아가 복잡한 비동기 파이프라인으로 확장될 수 있다.
'잡(job)기술' 카테고리의 다른 글
| 영상 스트리밍의 숨겨진 기술: RTSP over HTTP의 3가지 놀라운 사실 (0) | 2025.10.13 |
|---|---|
| I/O 기반 코루틴: 블로킹을 피하는 우아한 방법 (0) | 2025.09.04 |
| C++20 코루틴으로 직접 구현하는 Generator (0) | 2025.09.03 |
| CMake --preset: 더 이상 길고 복잡한 빌드 명령어에 시달리지 말자 (1) | 2025.06.28 |
| Poetry로 Python 프로젝트 환경 만들기 (2) | 2025.06.24 |