C++20의 코루틴으로 비동기 프로그래밍 간단 구현

C++20에서 새롭게 도입된 코루틴(Coroutines)은 기존의 쓰레드 기반 비동기 프로그래밍보다 간결한 코드 작성을 가능하게 합니다. 기존에는 std::thread, std::async, 또는 콜백(callback) 방식을 사용하여 비동기 작업을 처리했지만, 코드가 복잡해지고 가독성이 떨어지는 문제가 있었습니다.

코루틴을 활용하면 함수 실행을 중단하고(co_await), 필요할 때 다시 재개할 수 있어, 상태를 관리하면서도 비동기 작업을 자연스럽게 구현할 수 있습니다. 특히 네트워크 요청, 파일 I/O, UI 이벤트 처리, 게임 루프 등에서 효율적으로 사용할 수 있습니다.

이 기사에서는 C++20 코루틴의 기본 개념부터 실용적인 예제, 기존의 비동기 처리 방식과의 비교, 성능 분석 등을 다루며, 코루틴을 활용한 최적의 비동기 프로그래밍 방법을 소개합니다.

코루틴이란 무엇인가?

코루틴(Coroutine)은 함수의 실행을 중단했다가 다시 재개할 수 있는 기능을 제공하는 특수한 형태의 함수입니다. 전통적인 함수는 호출되면 끝날 때까지 실행되지만, 코루틴은 중간에 실행을 멈추고(suspend), 필요할 때 다시 실행을 재개(resume)할 수 있습니다.

기존 비동기 프로그래밍과의 차이점

일반적인 비동기 프로그래밍에서는 다음과 같은 방법이 사용됩니다.

  1. 쓰레드 기반 (std::thread)
  • 별도의 쓰레드를 생성하여 작업을 수행하지만, 스레드 개수가 많아지면 오버헤드가 증가합니다.
  1. 콜백 방식 (Callback)
  • 비동기 작업이 완료되면 특정 함수(콜백)를 호출하는 방식으로, 코드가 복잡해지고 가독성이 떨어지는 단점이 있습니다.
  1. 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

문제점:

  1. 콜백 중첩 문제
  • 콜백 내부에서 또 다른 콜백을 호출해야 하는 경우 코드가 복잡해지고 가독성이 저하됩니다.
  1. 코드 흐름이 직관적이지 않음
  • 파일을 읽고 처리하는 코드가 한 곳에 있지 않고 분리되어 있어, 유지보수가 어렵습니다.
  1. 오류 처리 어려움
  • 콜백 체인이 깊어지면 예외 처리가 복잡해집니다.

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
파일 읽기 완료

개선된 점:

  1. 가독성이 좋아짐
  • co_await을 사용하여 동기 코드처럼 직관적인 흐름을 유지할 수 있습니다.
  1. 콜백 중첩 제거
  • 콜백 함수 없이 코루틴 내부에서 파일을 읽고, 작업을 이어서 수행합니다.
  1. 예외 처리 용이
  • 코루틴 내부에서 try-catch를 사용하면 예외 처리를 쉽게 할 수 있습니다.

3. 콜백과 코루틴 비교

비교 항목콜백 방식C++20 코루틴 방식
코드 가독성복잡함 (콜백 중첩 발생)간결함 (co_await으로 표현)
오류 처리어렵고 복잡함예외 처리가 쉬움
비동기 흐름비직관적 (콜백 체인)동기 코드처럼 자연스러움
유지보수성어려움용이함
성능비슷하거나 약간 빠름약간의 오버헤드 있음

요약

  • 기존 콜백 방식은 콜백 지옥 문제로 인해 코드 가독성과 유지보수성이 떨어짐.
  • C++20 코루틴을 활용하면 co_await을 통해 비동기 흐름을 동기 코드처럼 표현할 수 있음.
  • 예외 처리가 용이하며, 코드가 간결해지고 가독성이 향상됨.
  • 성능 면에서는 약간의 오버헤드가 존재할 수 있지만, 유지보수성과 가독성이 향상되므로 현대적인 C++에서는 코루틴이 더 권장됨.

다음 장에서는 C++ 코루틴과 std::future, std::async 방식과의 차이점을 비교해보겠습니다.

C++ 코루틴과 std::future, std::async 비교

C++에서는 기존에도 std::futurestd::async를 이용한 비동기 프로그래밍이 가능했습니다. 그렇다면, C++20 코루틴이 기존 std::futurestd::async 방식과 어떻게 다른지 비교해보겠습니다.


1. std::asyncstd::future의 기존 방식

C++11에서는 std::asyncstd::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

특징 및 문제점:

  1. std::async새로운 쓰레드를 생성하여 작업을 실행하지만, 필요하지 않은 경우에도 쓰레드를 생성할 수 있음.
  2. result.get()을 호출하면 작업이 완료될 때까지 블로킹(blocking)됨 → 비동기 코드의 장점이 줄어듦.
  3. 연속적인 비동기 작업을 처리하려면 추가적인 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::futureC++20 코루틴
비동기 표현std::future를 사용하여 값 반환co_await로 자연스럽게 대기
쓰레드 생성필요하지 않아도 새 쓰레드 생성 가능쓰레드 사용 없이 코루틴 컨텍스트 유지
성능쓰레드 오버헤드가 있음컨텍스트 전환이 가벼움
연속적인 비동기 실행여러 개의 future를 관리해야 함co_await으로 간단하게 처리
코드 가독성get()을 호출해야 하므로 코드가 복잡동기 코드처럼 자연스럽게 표현 가능

4. 코루틴을 선택해야 하는 경우

C++20 코루틴이 더 적합한 경우:

  • 비동기 로직을 동기 코드처럼 자연스럽게 표현하고 싶을 때
  • 불필요한 쓰레드 생성 없이 가벼운 비동기 처리가 필요할 때
  • 여러 개의 비동기 작업을 순차적으로 처리해야 할 때

std::async를 사용할 수 있는 경우:

  • 기존 C++11/14 코드에서 간단한 비동기 처리를 추가하고 싶을 때
  • 멀티스레딩을 적극적으로 활용하여 병렬 처리 성능을 높이고 싶을 때

요약

  1. 기존 std::async는 비동기 작업을 쉽게 만들 수 있지만, 불필요한 쓰레드 생성과 블로킹 문제가 있음.
  2. C++20 코루틴은 co_await을 사용하여 자연스럽게 비동기 흐름을 표현할 수 있음.
  3. 쓰레드 없이 컨텍스트 전환이 가벼워 성능적으로 유리하며, 연속적인 비동기 작업을 쉽게 구현할 수 있음.

다음 장에서는 코루틴의 성능과 메모리 관리에 대해 자세히 분석해보겠습니다.

성능과 메모리 관리

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 newoperator 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::futurestd::async와 비교하여 코루틴이 가지는 장점과 성능 최적화 방법을 살펴보았습니다.

핵심 내용 정리

  1. 코루틴의 기본 개념
  • 코루틴은 co_await, co_yield, co_return을 활용하여 비동기 작업을 동기 코드처럼 간결하게 작성할 수 있음.
  1. 비동기 프로그래밍에서의 활용 사례
  • 네트워크 요청, 파일 I/O, 게임 루프, 이벤트 기반 프로그래밍 등에서 자연스러운 흐름을 유지할 수 있음.
  1. 기존 방식과의 비교
  • std::asyncstd::future 대비 코루틴은 불필요한 쓰레드 생성을 줄이고 가볍게 실행 가능.
  • 콜백 방식보다 가독성이 뛰어나고 예외 처리가 쉬움.
  1. 코루틴의 성능과 메모리 최적화
  • 코루틴은 힙 할당을 최소화하고 std::coroutine_handle을 활용하면 메모리를 효율적으로 관리 가능.
  • 컨텍스트 전환 비용이 낮아 쓰레드 기반의 비동기 처리보다 성능적으로 유리.
  1. 실전 예제
  • HTTP 요청 처리, 파일 다운로드, 게임 루프 애니메이션 등 실무에서 유용한 사례를 구현하여 코루틴의 활용성을 확인.

결론
C++20의 코루틴을 활용하면 더 간결하고 직관적인 비동기 코드를 작성할 수 있으며, 성능 최적화 및 유지보수성 향상에도 큰 도움이 됩니다. 기존의 콜백 방식이나 std::async 기반 코드보다 구현이 쉽고 가독성이 뛰어나므로, 최신 C++ 프로젝트에서는 적극적으로 활용하는 것이 바람직합니다.

👉 C++20 코루틴을 활용하여 비동기 프로그래밍을 한층 더 발전시키세요! 🚀

목차