[C++20 새기능 소개] 코루틴 (Coroutines)

C++20의 새로운 기능들을 소개하는 시리즈의 네 번째 글에 오신 것을 환영합니다. 이번 글에서는 비동기 프로그래밍과 협업적인 작업을 더욱 쉽게 만들어 줄 코루틴(Coroutines)에 대해 자세히 알아보겠습니다.

코루틴(Coroutines)이란 무엇인가요?

코루틴은 함수의 실행을 일시 중단하고, 나중에 다시 재개할 수 있는 특별한 형태의 함수입니다. 이는 비동기 프로그래밍, 제너레이터, 이터레이터 등을 구현할 때 매우 유용합니다. 코루틴을 사용하면 복잡한 상태 관리나 콜백 함수 없이도 자연스럽게 비동기 동작을 구현할 수 있습니다.

왜 코루틴을 사용해야 할까요?

기존의 비동기 프로그래밍은 콜백 지옥이나 복잡한 상태 머신을 만들게 되어 코드의 가독성과 유지 보수성이 떨어졌습니다. 코루틴을 사용하면 이러한 문제를 해결하고, 동기 코드처럼 읽기 쉽고 간결한 비동기 코드를 작성할 수 있습니다.

간단한 예제

제너레이터 구현

#include <coroutine>
#include <iostream>

template<typename T>
struct Generator {
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;

    struct promise_type {
        T current_value;

        std::suspend_always yield_value(T value) {
            current_value = value;
            return {};
        }

        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        Generator get_return_object() {
            return Generator{handle_type::from_promise(*this)};
        }

        void return_void() {}
        void unhandled_exception() {
            std::exit(1);
        }
    };

    handle_type coro_handle;

    Generator(handle_type h) : coro_handle(h) {}
    ~Generator() {
        if (coro_handle) coro_handle.destroy();
    }

    bool move_next() {
        if (coro_handle.done())
            return false;
        coro_handle.resume();
        return !coro_handle.done();
    }

    T current_value() {
        return coro_handle.promise().current_value;
    }
};

Generator<int> getSequence(int max) {
    for (int i = 0; i <= max; ++i) {
        co_yield i;
    }
}

int main() {
    auto gen = getSequence(5);
    while (gen.move_next()) {
        std::cout << gen.current_value() << " ";
    }
    // 출력: 0 1 2 3 4 5
    return 0;
}

위 예제에서는 코루틴을 사용하여 getSequence라는 제너레이터 함수를 구현했습니다. co_yield 키워드를 사용하여 값을 생성하고, 호출자는 이터레이터처럼 값을 받아올 수 있습니다.

코루틴의 주요 구성 요소

1. co_await, co_yield, co_return 키워드

  • co_await: 비동기 작업의 완료를 기다립니다.
  • co_yield: 값을 생성하고 호출자에게 제어를 반환합니다.
  • co_return: 코루틴을 종료하고 값을 반환합니다.

2. promise_type과 std::coroutine_handle

  • promise_type: 코루틴의 상태와 결과를 관리하는 객체입니다.
  • std::coroutine_handle: 코루틴을 제어하고 상태를 관리하는 핸들입니다.

3. suspend_always와 suspend_never

  • std::suspend_always: 항상 실행을 일시 중단합니다.
  • std::suspend_never: 실행을 일시 중단하지 않습니다.

비동기 작업 예제

#include <coroutine>
#include <iostream>
#include <thread>
#include <chrono>

struct SleepCoroutine {
    struct promise_type {
        SleepCoroutine get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

SleepCoroutine sleep(int milliseconds) {
    std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
    co_return;
}

int main() {
    std::cout << "작업 시작\n";
    sleep(1000);
    std::cout << "1초 후 작업 완료\n";
    return 0;
}

위 예제에서는 sleep 코루틴을 사용하여 1초 동안 대기한 후 메시지를 출력합니다.

코루틴을 활용한 비동기 I/O

코루틴은 네트워크 통신이나 파일 입출력 같은 비동기 I/O 작업에서 강력한 기능을 제공합니다. 예를 들어, 비동기 파일 읽기 함수를 코루틴으로 구현하면 다음과 같이 사용할 수 있습니다.

async_task<void> readFileAsync(const std::string& filename) {
    std::string content = co_await async_read_file(filename);
    std::cout << content;
}

위 코드에서 co_await를 통해 파일 읽기 작업이 완료될 때까지 기다립니다.

코루틴의 장점

  • 가독성 향상: 비동기 코드를 동기 코드처럼 작성할 수 있습니다.
  • 복잡도 감소: 상태 머신이나 콜백 함수의 복잡성을 줄여줍니다.
  • 성능 최적화: 스레드 오버헤드 없이 비동기 작업을 수행할 수 있습니다.

주의 사항

  • 컴파일러 지원: 코루틴은 최신 컴파일러와 표준 라이브러리의 지원이 필요합니다.
  • 메모리 관리: 코루틴의 생명 주기와 메모리 관리를 신중하게 해야 합니다.
  • 디버깅 난이도: 코루틴 내부의 디버깅이 복잡할 수 있습니다.

결론

C++20의 코루틴은 비동기 프로그래밍을 혁신적으로 개선하여 개발자들이 더욱 효율적이고 가독성 높은 코드를 작성할 수 있게 해줍니다. 코루틴을 활용하여 비동기 작업을 간단하고 직관적으로 구현해 보세요.

반응형