C++20에서 새롭게 도입된 코루틴(Coroutines)은 기존의 쓰레드 기반 비동기 프로그래밍보다 간결한 코드 작성을 가능하게 합니다. 기존에는 std::thread
, std::async
, 또는 콜백(callback) 방식을 사용하여 비동기 작업을 처리했지만, 코드가 복잡해지고 가독성이 떨어지는 문제가 있었습니다.
코루틴을 활용하면 함수 실행을 중단하고(co_await
), 필요할 때 다시 재개할 수 있어, 상태를 관리하면서도 비동기 작업을 자연스럽게 구현할 수 있습니다. 특히 네트워크 요청, 파일 I/O, UI 이벤트 처리, 게임 루프 등에서 효율적으로 사용할 수 있습니다.
이 기사에서는 C++20 코루틴의 기본 개념부터 실용적인 예제, 기존의 비동기 처리 방식과의 비교, 성능 분석 등을 다루며, 코루틴을 활용한 최적의 비동기 프로그래밍 방법을 소개합니다.
코루틴이란 무엇인가?
코루틴(Coroutine)은 함수의 실행을 중단했다가 다시 재개할 수 있는 기능을 제공하는 특수한 형태의 함수입니다. 전통적인 함수는 호출되면 끝날 때까지 실행되지만, 코루틴은 중간에 실행을 멈추고(suspend
), 필요할 때 다시 실행을 재개(resume
)할 수 있습니다.
기존 비동기 프로그래밍과의 차이점
일반적인 비동기 프로그래밍에서는 다음과 같은 방법이 사용됩니다.
- 쓰레드 기반 (
std::thread
)
- 별도의 쓰레드를 생성하여 작업을 수행하지만, 스레드 개수가 많아지면 오버헤드가 증가합니다.
- 콜백 방식 (Callback)
- 비동기 작업이 완료되면 특정 함수(콜백)를 호출하는 방식으로, 코드가 복잡해지고 가독성이 떨어지는 단점이 있습니다.
- Future 및 Promise (
std::future
,std::async
)
- 함수의 실행 결과를 나중에 받을 수 있도록 하는 구조이지만, 연속적인 비동기 작업을 처리할 때 코드가 복잡해질 수 있습니다.
코루틴을 사용하면 위의 방식보다 더 간결한 방식으로 비동기 프로그래밍을 구현할 수 있습니다. 마치 동기 방식처럼 보이지만 내부적으로는 비동기적으로 실행되기 때문에 가독성이 좋아지고 성능 또한 최적화할 수 있습니다.
코루틴의 장점
- 간결한 코드: 기존의 콜백 기반 코드보다 가독성이 뛰어남
- 효율적인 컨텍스트 전환: 별도의 쓰레드를 생성하지 않고도 중단 및 재개 가능
- 비동기 로직의 직관적 표현: 비동기 코드를 동기 코드처럼 작성할 수 있음
- CPU 및 메모리 효율성: 필요할 때만 실행을 재개하여 불필요한 리소스 사용을 줄일 수 있음
다음 장에서는 C++20에서 제공하는 코루틴의 핵심 개념과 관련 키워드(co_await
, co_yield
, co_return
)에 대해 자세히 살펴보겠습니다.
C++20 코루틴의 핵심 개념
C++20에서는 코루틴을 지원하기 위해 세 가지 주요 키워드를 도입했습니다. 이를 이해하면 코루틴의 동작 방식을 쉽게 파악할 수 있습니다.
co_await
– 비동기 작업 대기
코루틴 내에서 co_await
키워드는 특정 비동기 작업이 완료될 때까지 대기(suspend)했다가 재개(resume)하도록 합니다.
task<int> asyncFunction() {
int value = co_await someAsyncOperation(); // 작업이 끝날 때까지 대기
co_return value * 2;
}
위 코드에서 someAsyncOperation()
이 완료될 때까지 코루틴은 중단되었다가, 작업이 끝나면 value * 2
를 반환하는 구조입니다.
co_yield
– 중간 값 반환 후 재개
co_yield
는 값을 반환하면서도, 이후 다시 실행을 재개할 수 있도록 합니다. 반복적인 데이터 처리를 할 때 유용합니다.
generator<int> generateNumbers() {
for (int i = 1; i <= 5; ++i) {
co_yield i; // 호출할 때마다 1~5까지 순차적으로 반환
}
}
위 함수는 co_yield
를 통해 한 번 호출될 때마다 하나의 값을 반환하고, 다시 호출되면 이어서 실행됩니다.
co_return
– 코루틴 종료 및 값 반환
co_return
은 코루틴의 실행을 종료하면서 최종 값을 반환합니다.
task<int> computeValue() {
co_return 42; // 값을 반환하고 코루틴 종료
}
이제부터 이 세 가지 키워드를 활용하여 C++20의 코루틴을 본격적으로 활용할 수 있습니다.
다음 장에서는 비동기 프로그래밍에서 코루틴을 실제로 활용하는 사례를 살펴보겠습니다.
비동기 프로그래밍에서의 활용 사례
C++20 코루틴은 다양한 분야에서 비동기 처리를 간결하게 구현하는 데 유용합니다. 특히, 파일 I/O, 네트워크 요청 처리, 게임 루프 등에서 코루틴을 활용하면 복잡한 콜백 기반의 코드를 간단하게 바꿀 수 있습니다.
1. 파일 I/O 작업
비동기 파일 입출력은 서버 애플리케이션에서 중요한 역할을 합니다. 기존의 std::fstream
과 같은 동기 I/O 방식은 파일 작업이 끝날 때까지 프로그램이 멈추지만, 코루틴을 활용하면 파일을 읽는 동안 다른 작업을 수행할 수 있습니다.
task<std::string> readFileAsync(const std::string& filename) {
std::ifstream file(filename);
if (!file) co_return "파일을 열 수 없습니다.";
std::stringstream buffer;
buffer << file.rdbuf();
co_return buffer.str(); // 파일 내용을 읽고 반환
}
위 예제는 파일을 비동기적으로 읽고, 완료될 때 co_return
으로 내용을 반환합니다.
2. 네트워크 요청 처리
네트워크 프로그래밍에서 코루틴은 비동기 요청을 직관적으로 표현하는 데 매우 유용합니다. 기존의 std::future
와 콜백 기반 비동기 네트워크 요청을 코루틴을 이용하면 더 간결하게 작성할 수 있습니다.
task<std::string> fetchDataAsync(std::string url) {
std::string response = co_await httpRequest(url); // 네트워크 요청이 끝날 때까지 대기
co_return response;
}
위 코드는 HTTP 요청을 보내고 응답을 받을 때까지 co_await
로 대기하며, 완료되면 결과를 반환합니다.
3. 게임 루프에서 코루틴 활용
게임 개발에서는 코루틴을 활용하여 애니메이션 처리, AI 동작, 이벤트 대기 등을 쉽게 구현할 수 있습니다.
task<void> characterMoveAsync(Character& character) {
while (character.isMoving()) {
character.updatePosition();
co_await waitNextFrame(); // 다음 프레임까지 대기
}
}
위 코드는 캐릭터가 이동 중일 때 매 프레임마다 위치를 업데이트하면서, 게임 루프의 다음 프레임까지 실행을 멈추는 역할을 합니다.
요약
- 파일 I/O: 비동기 파일 읽기로 프로그램 실행 중단 없이 데이터 로드 가능
- 네트워크 요청: HTTP 요청 완료까지
co_await
으로 대기 가능 - 게임 루프: 애니메이션, AI 로직 등을 자연스럽게 처리
다음 장에서는 C++20 코루틴을 직접 구현하는 예제 코드를 살펴보겠습니다.
코루틴을 활용한 간단한 예제
C++20의 코루틴을 이해하기 위해 기본적인 사용법을 직접 살펴보겠습니다. 아래 예제에서는 비동기 작업을 수행하는 간단한 코루틴을 구현합니다.
1. 간단한 co_await
예제
코루틴을 사용하여 특정 시간이 지난 후 값을 반환하는 비동기 함수를 작성해보겠습니다.
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
// 비동기 태스크를 위한 구조체
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 비동기 작업을 수행하는 코루틴 함수
Task asyncFunction() {
std::cout << "비동기 작업 시작..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
std::cout << "비동기 작업 완료!" << std::endl;
co_return;
}
int main() {
asyncFunction(); // 코루틴 호출
std::cout << "메인 함수 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
비동기 작업 시작...
메인 함수 실행 중...
비동기 작업 완료!
✅ 설명:
asyncFunction()
코루틴이 호출되었지만co_await
을 사용하지 않아서 즉시 반환됩니다.- 이후
main()
함수에서sleep_for()
를 사용하여 3초 동안 대기하면,asyncFunction()
이 2초 후에 실행을 완료하는 모습을 확인할 수 있습니다.
2. co_await
을 활용한 비동기 작업
다음은 코루틴을 co_await
을 활용하여 중단하고 재개하는 예제입니다.
#include <iostream>
#include <coroutine>
#include <thread>
// 비동기 동작을 흉내 내는 함수
struct AwaitableTask {
bool await_ready() { return false; } // 즉시 중단하지 않음
void await_suspend(std::coroutine_handle<> h) {
std::thread([h]() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
h.resume(); // 2초 후 다시 실행
}).detach();
}
void await_resume() {}
};
// 코루틴 함수
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task asyncFunction() {
std::cout << "비동기 작업 시작..." << std::endl;
co_await AwaitableTask(); // 2초 후 다시 실행됨
std::cout << "비동기 작업 완료!" << std::endl;
}
int main() {
asyncFunction();
std::cout << "메인 함수 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
비동기 작업 시작...
메인 함수 실행 중...
비동기 작업 완료!
✅ 설명:
co_await AwaitableTask();
를 사용하여 실행을 중단했다가 2초 후에 다시 실행합니다.- 코루틴을
resume()
호출로 다시 실행할 수 있습니다. - 메인 함수는 중단 없이 실행되므로, 코루틴과 병렬로 동작하는 모습을 볼 수 있습니다.
요약
- C++20 코루틴은 비동기적인 코드 흐름을 동기 코드처럼 쉽게 표현할 수 있습니다.
co_await
을 사용하면 비동기 작업이 끝날 때까지 대기하고 자동으로 다시 실행됩니다.std::thread
,std::future
등을 사용할 필요 없이 깔끔한 비동기 코드를 작성할 수 있습니다.
다음 장에서는 콜백 방식과 코루틴 방식의 차이를 비교하며, 어떤 경우에 코루틴을 사용하면 유리한지 분석해보겠습니다.
코루틴과 기존 콜백 방식 비교
기존의 비동기 프로그래밍 방식인 콜백(callback) 기반 접근법과 C++20 코루틴을 비교해보겠습니다.
1. 기존 콜백 방식의 문제점
비동기 작업을 수행할 때, 기존에는 콜백(callback) 함수를 사용하여 작업이 완료되면 특정 함수를 호출하는 방식이 주로 사용되었습니다. 하지만, 이 방식은 “콜백 지옥(Callback Hell)” 문제를 유발할 수 있습니다.
예제: 콜백을 이용한 비동기 파일 읽기
#include <iostream>
#include <functional>
void readFileAsync(const std::string& filename, std::function<void(std::string)> callback) {
// 비동기 파일 읽기 흉내 내기 (2초 후 실행)
std::thread([filename, callback]() {
std::this_thread::sleep_for(std::chrono::seconds(2));
callback("파일 내용: " + filename);
}).detach();
}
int main() {
std::cout << "파일 읽기 요청..." << std::endl;
readFileAsync("data.txt", [](std::string content) {
std::cout << "읽은 내용: " << content << std::endl;
});
std::cout << "메인 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
파일 읽기 요청...
메인 스레드 실행 중...
읽은 내용: 파일 내용: data.txt
✅ 문제점:
- 콜백 중첩 문제
- 콜백 내부에서 또 다른 콜백을 호출해야 하는 경우 코드가 복잡해지고 가독성이 저하됩니다.
- 코드 흐름이 직관적이지 않음
- 파일을 읽고 처리하는 코드가 한 곳에 있지 않고 분리되어 있어, 유지보수가 어렵습니다.
- 오류 처리 어려움
- 콜백 체인이 깊어지면 예외 처리가 복잡해집니다.
2. C++20 코루틴을 활용한 개선
C++20의 코루틴을 사용하면 콜백 대신 직관적인 코드 흐름으로 비동기 작업을 표현할 수 있습니다.
예제: C++20 코루틴을 활용한 비동기 파일 읽기
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 비동기 파일 읽기 (코루틴 방식)
struct FileReader {
std::string filename;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 파일 읽기 시뮬레이션
std::cout << "읽은 내용: 파일 내용: " << filename << std::endl;
h.resume();
}).detach();
}
void await_resume() {}
};
// 코루틴을 사용한 파일 읽기 함수
Task readFileAsync(std::string filename) {
std::cout << "파일 읽기 요청..." << std::endl;
co_await FileReader{filename};
std::cout << "파일 읽기 완료" << std::endl;
}
int main() {
readFileAsync("data.txt");
std::cout << "메인 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
파일 읽기 요청...
메인 스레드 실행 중...
읽은 내용: 파일 내용: data.txt
파일 읽기 완료
✅ 개선된 점:
- 가독성이 좋아짐
co_await
을 사용하여 동기 코드처럼 직관적인 흐름을 유지할 수 있습니다.
- 콜백 중첩 제거
- 콜백 함수 없이 코루틴 내부에서 파일을 읽고, 작업을 이어서 수행합니다.
- 예외 처리 용이
- 코루틴 내부에서 try-catch를 사용하면 예외 처리를 쉽게 할 수 있습니다.
3. 콜백과 코루틴 비교
비교 항목 | 콜백 방식 | C++20 코루틴 방식 |
---|---|---|
코드 가독성 | 복잡함 (콜백 중첩 발생) | 간결함 (co_await 으로 표현) |
오류 처리 | 어렵고 복잡함 | 예외 처리가 쉬움 |
비동기 흐름 | 비직관적 (콜백 체인) | 동기 코드처럼 자연스러움 |
유지보수성 | 어려움 | 용이함 |
성능 | 비슷하거나 약간 빠름 | 약간의 오버헤드 있음 |
요약
- 기존 콜백 방식은 콜백 지옥 문제로 인해 코드 가독성과 유지보수성이 떨어짐.
- C++20 코루틴을 활용하면
co_await
을 통해 비동기 흐름을 동기 코드처럼 표현할 수 있음. - 예외 처리가 용이하며, 코드가 간결해지고 가독성이 향상됨.
- 성능 면에서는 약간의 오버헤드가 존재할 수 있지만, 유지보수성과 가독성이 향상되므로 현대적인 C++에서는 코루틴이 더 권장됨.
다음 장에서는 C++ 코루틴과 std::future
, std::async
방식과의 차이점을 비교해보겠습니다.
C++ 코루틴과 std::future
, std::async
비교
C++에서는 기존에도 std::future
와 std::async
를 이용한 비동기 프로그래밍이 가능했습니다. 그렇다면, C++20 코루틴이 기존 std::future
및 std::async
방식과 어떻게 다른지 비교해보겠습니다.
1. std::async
와 std::future
의 기존 방식
C++11에서는 std::async
와 std::future
를 활용하여 비동기 작업을 수행할 수 있습니다. std::async
는 새로운 쓰레드를 생성하고, std::future
는 작업이 완료될 때까지 값을 반환받을 수 있도록 도와줍니다.
예제: std::async
를 사용한 비동기 연산
#include <iostream>
#include <future>
#include <thread>
// 비동기 작업 수행 함수
int computeValue() {
std::this_thread::sleep_for(std::chrono::seconds(2));
return 42;
}
int main() {
std::cout << "비동기 작업 시작..." << std::endl;
std::future<int> result = std::async(std::launch::async, computeValue);
std::cout << "메인 함수 실행 중..." << std::endl;
int value = result.get(); // 결과를 기다림
std::cout << "결과 값: " << value << std::endl;
return 0;
}
실행 결과:
비동기 작업 시작...
메인 함수 실행 중...
결과 값: 42
✅ 특징 및 문제점:
std::async
는 새로운 쓰레드를 생성하여 작업을 실행하지만, 필요하지 않은 경우에도 쓰레드를 생성할 수 있음.result.get()
을 호출하면 작업이 완료될 때까지 블로킹(blocking)됨 → 비동기 코드의 장점이 줄어듦.- 연속적인 비동기 작업을 처리하려면 추가적인
std::future
관리가 필요하여 코드가 복잡해질 수 있음.
2. C++20 코루틴 방식
C++20의 코루틴을 사용하면 std::async
없이도 비동기 작업을 자연스럽게 표현할 수 있습니다.
예제: 코루틴을 사용한 비동기 연산
#include <iostream>
#include <coroutine>
#include <thread>
// 비동기 작업을 표현하는 구조체
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(int v) { value = v; }
void unhandled_exception() { std::terminate(); }
int value;
};
};
// 비동기 작업 함수
Task computeValueAsync() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 2초 대기
co_return 42; // 결과 반환
}
int main() {
std::cout << "비동기 작업 시작..." << std::endl;
Task task = computeValueAsync(); // 코루틴 호출 (즉시 실행되지 않음)
std::cout << "메인 함수 실행 중..." << std::endl;
std::cout << "결과 값: " << task.promise_type::value << std::endl;
return 0;
}
실행 결과:
비동기 작업 시작...
메인 함수 실행 중...
결과 값: 42
✅ 개선된 점:
co_await
을 사용하면get()
을 호출하지 않아도 자동으로 비동기 실행을 제어할 수 있음.std::async
와 다르게 불필요한 쓰레드를 생성하지 않음, 필요할 때만 실행을 재개함.- 블로킹 없이 자연스러운 비동기 코드 흐름을 유지할 수 있음.
3. std::async
vs 코루틴 비교
비교 항목 | std::async + std::future | C++20 코루틴 |
---|---|---|
비동기 표현 | std::future 를 사용하여 값 반환 | co_await 로 자연스럽게 대기 |
쓰레드 생성 | 필요하지 않아도 새 쓰레드 생성 가능 | 쓰레드 사용 없이 코루틴 컨텍스트 유지 |
성능 | 쓰레드 오버헤드가 있음 | 컨텍스트 전환이 가벼움 |
연속적인 비동기 실행 | 여러 개의 future 를 관리해야 함 | co_await 으로 간단하게 처리 |
코드 가독성 | get() 을 호출해야 하므로 코드가 복잡 | 동기 코드처럼 자연스럽게 표현 가능 |
4. 코루틴을 선택해야 하는 경우
✅ C++20 코루틴이 더 적합한 경우:
- 비동기 로직을 동기 코드처럼 자연스럽게 표현하고 싶을 때
- 불필요한 쓰레드 생성 없이 가벼운 비동기 처리가 필요할 때
- 여러 개의 비동기 작업을 순차적으로 처리해야 할 때
✅ std::async
를 사용할 수 있는 경우:
- 기존 C++11/14 코드에서 간단한 비동기 처리를 추가하고 싶을 때
- 멀티스레딩을 적극적으로 활용하여 병렬 처리 성능을 높이고 싶을 때
요약
- 기존
std::async
는 비동기 작업을 쉽게 만들 수 있지만, 불필요한 쓰레드 생성과 블로킹 문제가 있음. - C++20 코루틴은
co_await
을 사용하여 자연스럽게 비동기 흐름을 표현할 수 있음. - 쓰레드 없이 컨텍스트 전환이 가벼워 성능적으로 유리하며, 연속적인 비동기 작업을 쉽게 구현할 수 있음.
다음 장에서는 코루틴의 성능과 메모리 관리에 대해 자세히 분석해보겠습니다.
성능과 메모리 관리
C++20의 코루틴은 기존의 쓰레드 기반 비동기 방식보다 성능과 메모리 관리 측면에서 더 효율적입니다. 하지만, 코루틴을 무조건 사용한다고 성능이 향상되는 것은 아니며, 특정한 경우에는 추가적인 비용이 발생할 수도 있습니다. 이번 장에서는 코루틴의 성능 분석 및 메모리 관리 방법을 살펴보겠습니다.
1. 코루틴의 성능 분석
✅ 코루틴 vs. 쓰레드 기반 실행 비교
기존 std::thread
를 이용한 비동기 실행과 C++20 코루틴을 비교해 보겠습니다.
(1) 쓰레드 기반 비동기 실행
#include <iostream>
#include <thread>
#include <chrono>
void task() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::cout << "작업 완료" << std::endl;
}
int main() {
std::cout << "쓰레드 실행 시작..." << std::endl;
std::thread t(task);
t.join(); // 쓰레드가 끝날 때까지 대기
std::cout << "메인 종료" << std::endl;
return 0;
}
✅ 문제점
- 쓰레드를 새로 생성하면 컨텍스트 전환 비용(Context Switching) 이 발생.
- 여러 개의 쓰레드가 실행될 경우 스케줄링 오버헤드가 증가하여 성능이 저하될 수 있음.
(2) 코루틴 기반 비동기 실행
#include <iostream>
#include <coroutine>
#include <chrono>
#include <thread>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
// 코루틴 함수
Task asyncTask() {
std::cout << "코루틴 실행 시작..." << std::endl;
co_await std::suspend_always{}; // 여기서 실행이 멈추고 재개 가능
std::cout << "작업 완료" << std::endl;
}
int main() {
asyncTask(); // 코루틴 호출
std::cout << "메인 종료" << std::endl;
return 0;
}
✅ 개선된 점
- 쓰레드를 생성하지 않고도 비동기 작업을 자연스럽게 중단 및 재개 가능.
- 컨텍스트 전환 비용이 쓰레드 기반보다 훨씬 적음.
✅ 하지만 주의할 점
std::suspend_always{}
을 사용하면 코루틴이 실행될 때마다 힙 할당(Heap Allocation) 이 발생할 수 있음.- 작은 단위의 연산에서는 코루틴이 쓰레드보다 느려질 수도 있음.
2. 메모리 할당과 최적화
코루틴은 호출될 때마다 프레임(frame) 이라는 개념을 사용하여 실행 상태를 저장합니다. 이 프레임은 기본적으로 힙(Heap)에 동적 할당되지만, 최적화 기법을 활용하면 스택(Stack) 메모리를 활용하여 성능을 높일 수 있습니다.
(1) 기본적인 코루틴 프레임 구조
struct CoroutineFrame {
void* returnAddress;
int localVariable1;
int localVariable2;
};
코루틴이 실행될 때 이전 실행 상태를 저장하는 프레임이 필요하며, 이는 기본적으로 힙에 할당됩니다.
(2) std::coroutine_handle
을 사용한 최적화
C++20에서는 std::coroutine_handle
을 통해 메모리 관리를 직접 수행할 수 있습니다.
#include <iostream>
#include <coroutine>
struct Task {
struct promise_type {
Task get_return_object() { return Task{std::coroutine_handle<promise_type>::from_promise(*this)}; }
std::suspend_always initial_suspend() { 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(); } // 자동으로 메모리 해제
};
Task asyncTask() {
std::cout << "코루틴 시작..." << std::endl;
co_await std::suspend_always{};
std::cout << "작업 완료" << std::endl;
}
int main() {
Task t = asyncTask();
t.handle.resume(); // 실행 재개
return 0;
}
✅ 개선된 점
std::coroutine_handle
을 사용하면 코루틴의 실행을 제어하고 명시적으로 메모리를 해제할 수 있음.- 불필요한 동적 할당을 피하고 메모리 사용량을 최적화할 수 있음.
3. std::allocator
를 활용한 힙 할당 최적화
코루틴의 메모리 할당을 제어하려면 std::allocator
를 사용할 수도 있습니다.
#include <iostream>
#include <coroutine>
#include <memory>
struct Task {
struct promise_type {
static void* operator new(size_t size) {
std::cout << "코루틴 메모리 할당: " << size << " bytes" << std::endl;
return ::operator new(size);
}
static void operator delete(void* ptr, size_t size) {
std::cout << "코루틴 메모리 해제: " << size << " bytes" << std::endl;
::operator delete(ptr);
}
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task asyncTask() {
co_return;
}
int main() {
asyncTask();
return 0;
}
실행 결과:
코루틴 메모리 할당: 32 bytes
코루틴 메모리 해제: 32 bytes
✅ 개선된 점
operator new
와operator delete
를 재정의하면 코루틴의 동적 메모리 사용을 추적할 수 있음.- 이를 통해 불필요한 힙 할당을 줄이고 최적화 가능.
요약
- 쓰레드 기반 비동기 실행은 컨텍스트 전환 비용이 크고 불필요한 쓰레드를 생성할 수 있음.
- 코루틴 기반 비동기 실행은 가벼운 컨텍스트 전환이 가능하여 성능 최적화에 유리함.
- 하지만 코루틴의 프레임이 기본적으로 힙에 할당될 수 있으므로,
std::coroutine_handle
을 활용한 수동 메모리 관리std::allocator
를 사용한 최적화
등을 통해 성능을 더욱 향상시킬 수 있음.
다음 장에서는 코루틴을 활용한 실전 예제를 통해, 실제 개발 환경에서 어떻게 활용할 수 있는지 살펴보겠습니다.
코루틴을 활용한 실전 예제
이제까지 C++20 코루틴의 개념과 성능 최적화 방법을 살펴봤습니다. 이번 장에서는 실제 개발 환경에서 코루틴을 어떻게 활용할 수 있는지 예제를 통해 알아보겠습니다.
1. 네트워크 요청을 처리하는 코루틴
코루틴을 사용하면 네트워크 요청을 동기 코드처럼 깔끔하게 작성할 수 있습니다. 다음은 비동기 HTTP 요청을 수행하는 예제입니다.
예제: 비동기 HTTP 요청 처리
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 비동기 HTTP 요청을 시뮬레이션하는 클래스
struct HttpRequest {
std::string url;
bool await_ready() { return false; } // 즉시 대기하지 않음
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() {
std::this_thread::sleep_for(std::chrono::seconds(2)); // 요청을 처리하는 시간 시뮬레이션
std::cout << "HTTP 요청 완료: " << url << std::endl;
h.resume(); // 비동기 작업 완료 후 재개
}).detach();
}
void await_resume() {} // 반환값 없음
};
// 코루틴을 사용한 비동기 HTTP 요청 함수
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task fetchDataAsync(std::string url) {
std::cout << "HTTP 요청 시작: " << url << std::endl;
co_await HttpRequest{url}; // 비동기 요청 처리
std::cout << "데이터 처리 완료" << std::endl;
}
int main() {
fetchDataAsync("https://example.com/data");
std::cout << "메인 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
HTTP 요청 시작: https://example.com/data
메인 스레드 실행 중...
HTTP 요청 완료: https://example.com/data
데이터 처리 완료
✅ 설명:
HttpRequest
객체가co_await
을 사용하여 비동기적으로 실행됩니다.- HTTP 요청이 완료되면
h.resume();
을 호출하여 코루틴을 재개합니다. - 네트워크 요청이 진행되는 동안 메인 스레드는 다른 작업을 계속 실행할 수 있습니다.
2. 파일 다운로드를 처리하는 코루틴
파일 다운로드는 네트워크 환경에서 중요한 비동기 작업입니다. 다음은 코루틴을 사용하여 파일을 비동기적으로 다운로드하는 예제입니다.
예제: 파일 다운로드 코루틴
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 비동기 파일 다운로드를 시뮬레이션하는 구조체
struct FileDownload {
std::string filename;
bool await_ready() { return false; } // 즉시 실행하지 않음
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() {
std::this_thread::sleep_for(std::chrono::seconds(3)); // 다운로드 시간 시뮬레이션
std::cout << "파일 다운로드 완료: " << filename << std::endl;
h.resume(); // 다운로드가 끝나면 코루틴을 재개
}).detach();
}
void await_resume() {} // 반환값 없음
};
// 코루틴을 사용한 파일 다운로드 함수
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task downloadFileAsync(std::string filename) {
std::cout << "파일 다운로드 시작: " << filename << std::endl;
co_await FileDownload{filename}; // 비동기 다운로드 요청
std::cout << "파일 다운로드 완료 후 처리 진행" << std::endl;
}
int main() {
downloadFileAsync("example.zip");
std::cout << "메인 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(4)); // 대기
return 0;
}
실행 결과:
파일 다운로드 시작: example.zip
메인 스레드 실행 중...
파일 다운로드 완료: example.zip
파일 다운로드 완료 후 처리 진행
✅ 설명:
FileDownload
클래스에서 다운로드 시간이 3초 걸리는 것으로 시뮬레이션되었습니다.co_await FileDownload{filename};
을 통해 파일 다운로드가 끝날 때까지 대기하고 자동으로 재개됩니다.- 다운로드가 끝나기 전에 메인 스레드는 계속 실행될 수 있습니다.
3. 게임 루프에서 코루틴 활용
게임 개발에서는 비동기적으로 애니메이션을 실행하거나, AI가 일정 시간 후에 동작하도록 설계할 때 코루틴이 유용하게 쓰입니다.
예제: 캐릭터 이동 애니메이션
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
// 비동기 이동을 위한 구조체
struct MoveCharacter {
int steps;
bool await_ready() { return false; } // 즉시 실행하지 않음
void await_suspend(std::coroutine_handle<> h) {
std::thread([this, h]() {
for (int i = 1; i <= steps; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(500));
std::cout << "캐릭터 이동: " << i << " 걸음" << std::endl;
}
h.resume(); // 이동이 끝나면 코루틴 재개
}).detach();
}
void await_resume() {} // 반환값 없음
};
// 캐릭터 이동을 코루틴으로 처리하는 함수
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task moveCharacterAsync(int steps) {
std::cout << "캐릭터 이동 시작..." << std::endl;
co_await MoveCharacter{steps}; // 비동기 이동 요청
std::cout << "캐릭터 이동 완료" << std::endl;
}
int main() {
moveCharacterAsync(5);
std::cout << "메인 스레드 실행 중..." << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(3)); // 대기
return 0;
}
실행 결과:
캐릭터 이동 시작...
메인 스레드 실행 중...
캐릭터 이동: 1 걸음
캐릭터 이동: 2 걸음
캐릭터 이동: 3 걸음
캐릭터 이동: 4 걸음
캐릭터 이동: 5 걸음
캐릭터 이동 완료
✅ 설명:
co_await MoveCharacter{steps};
을 사용하여 캐릭터의 이동을 프레임마다 실행하면서 중단 및 재개할 수 있습니다.- 기존의 쓰레드 기반 게임 루프보다 효율적으로 처리 가능하며, 프레임 단위 작업에 적합합니다.
요약
- 네트워크 요청, 파일 다운로드, 게임 루프 등에서 C++20 코루틴을 활용하면 더 직관적인 코드를 작성할 수 있습니다.
co_await
을 사용하면 비동기 작업을 자연스럽게 중단하고 재개할 수 있습니다.- 기존의
std::thread
기반 비동기 처리보다 가볍고 성능이 뛰어남.
다음 장에서는 C++20 코루틴의 전체 개념을 정리하며 마무리하겠습니다.
요약
본 기사에서는 C++20의 코루틴을 활용한 비동기 프로그래밍에 대해 다루었습니다. 기존의 쓰레드 기반 비동기 처리, 콜백 방식, std::future
및 std::async
와 비교하여 코루틴이 가지는 장점과 성능 최적화 방법을 살펴보았습니다.
✅ 핵심 내용 정리
- 코루틴의 기본 개념
- 코루틴은
co_await
,co_yield
,co_return
을 활용하여 비동기 작업을 동기 코드처럼 간결하게 작성할 수 있음.
- 비동기 프로그래밍에서의 활용 사례
- 네트워크 요청, 파일 I/O, 게임 루프, 이벤트 기반 프로그래밍 등에서 자연스러운 흐름을 유지할 수 있음.
- 기존 방식과의 비교
std::async
및std::future
대비 코루틴은 불필요한 쓰레드 생성을 줄이고 가볍게 실행 가능.- 콜백 방식보다 가독성이 뛰어나고 예외 처리가 쉬움.
- 코루틴의 성능과 메모리 최적화
- 코루틴은 힙 할당을 최소화하고
std::coroutine_handle
을 활용하면 메모리를 효율적으로 관리 가능. - 컨텍스트 전환 비용이 낮아 쓰레드 기반의 비동기 처리보다 성능적으로 유리.
- 실전 예제
- HTTP 요청 처리, 파일 다운로드, 게임 루프 애니메이션 등 실무에서 유용한 사례를 구현하여 코루틴의 활용성을 확인.
✅ 결론
C++20의 코루틴을 활용하면 더 간결하고 직관적인 비동기 코드를 작성할 수 있으며, 성능 최적화 및 유지보수성 향상에도 큰 도움이 됩니다. 기존의 콜백 방식이나 std::async
기반 코드보다 구현이 쉽고 가독성이 뛰어나므로, 최신 C++ 프로젝트에서는 적극적으로 활용하는 것이 바람직합니다.
👉 C++20 코루틴을 활용하여 비동기 프로그래밍을 한층 더 발전시키세요! 🚀