6 분 소요

개요

  • 일시정지가 가능한 함수
  • 흐름
    • 호출자가 코루틴 호출
    • 코루틴은 일부 실행 후 일시중지(suspend)하고 리턴
    • 호출자가 재개(resumed)하면 일시중지 된 부분부터 실행
    • 일시중지와 재개를 반복
    • 파괴(destroy)
  • 제한
    • 가변 인자, plain return 문, placeholder return 타입(auto or concept) 사용 불가
    • main/constexpr/Consteval 함수, 생성자, 소멸자는 coroutine 불가
  • coroutine이 되려면 co_await, co_yield, co_return 중 하나는 사용하여야 함
    • co_await
      • 실행을 일시중지
      • co_await expr
        • expr은 awaitable이며 custom 가능
    • co_yield
      • 값을 반환하면서 실행을 일시중지
    • co_return
      • 값을 반환하면서 실행 완료
      • co_return 이후에는 재개 불가
    • done()
      • coroutines이 완료되면 true, 아니면 false 반환
  • 구성
    • promise object
      • 해당 객체를 통해 결과 또는 예외를 제출
    • coroutine handle
      • coroutine 외부에서 조작
      • 코루틴을 재개하거나 파괴
    • coroutine state
      • promise object
      • 매개변수(copy by value)
      • 일시정지 지점
      • 일시정지 지점에서의 로컬 및 임시 변수
  • 동작
    • 최초 실행
      • new 연산자를 사용하여 coroutine state 객체 할당
      • 매개변수를 coroutine state에 복사
        • 값 매개변수는 이동 또는 복사되고 참조 매개변수는 참조로 남음
        • 참조 객체의 수명이 끝난 후 coroutine이 재개되는 상황 주의
      • promise object의 생성자 호출
      • promise.get_return_object()를 호출하고 결과를 로컬 변수에 유지 후 처음 일시중지 시 호출자에게 반환
      • promise.initial_suspend() 호출하고 결과를 co_await에 전달
        • lazily-started coroutine은 suspend_always를 반환
          • suspend_always는 호출자로 제어권 넘김
        • eagerly-started coroutine은 suspend_never를 반환
          • suspend_never는 호출자로 제어권 넘기지 않고 바로 coroutine 내용 실행
    • co_await/co_yield
      • coroutine 리턴 타입으로 암시적 변환 후 호출자/재개자에게 반환
    • co_return
      • co_return expr에서 expr이 void면 promise.return_void() 아니면 promise.return_value(expr) 호출
        • return_void와 return_value는 동시 구현 불가
      • promise.final_suspend() 호출하고 결과를 co_await에 전달
    • 예외 발생
      • promise.unhandled_exception() 호출
      • promise.final_suspend() 호출하고 결과를 co_await에 전달
      • promise object 소멸자 호출
      • 함수 매개변수 복사본 소멸자 호출
      • delete 연산자를 이용하여 coroutine state 소멸
  • C++ coroutine은 stackless coroutine
    • stackful coroutine
      • coroutine 호출 시 자신만의 스택을 지님
      • coroutine의 라이프사이클은 호출자로부터 독립적
    • stackless coroutine
      • 호출자의 스택을 사용
      • coroutine의 라이프사이클은 호출자의 라이프사이클을 따름
      • coroutine state 유지에 필요한 정보는 힙 메모리에 할당


예제

  • 기본
    • 코드
        #include <coroutine>
        #include <future>
        #include <iostream>
        #include <memory>

        using namespace std;

        class Task1 {
            public:
                struct promise_type {
                        Task1 get_return_object() {
                            return Task1{
                                coroutine_handle<promise_type>::from_promise(*this)};
                        }
                        auto initial_suspend() { return suspend_always{}; }
                        auto final_suspend() noexcept { return suspend_always{}; }
                        void return_void() {}
                        void unhandled_exception() {}
                };

                coroutine_handle<promise_type> handler;

                Task1(coroutine_handle<promise_type> handler) : handler(handler) {
                    cout << "~~~~~~" << endl;
                }
                ~Task1() { this->handler.destroy(); }
        };

        class Task2 {
            public:
                struct promise_type {
                        int value;

                        Task2 get_return_object() {
                            return Task2{
                                coroutine_handle<promise_type>::from_promise(*this)};
                        }
                        auto initial_suspend() { return suspend_never{}; }
                        auto final_suspend() noexcept { return suspend_always{}; }
                        void return_value(int value) { this->value = value; }
                        void unhandled_exception() {}
                };

                coroutine_handle<promise_type> handler;

                Task2(coroutine_handle<promise_type> handler) : handler(handler) {}
                ~Task2() { this->handler.destroy(); }
        };

        class Task3 {
            public:
                struct promise_type {
                        int value = 0;

                        Task3 get_return_object() {
                            return Task3{
                                coroutine_handle<promise_type>::from_promise(*this)};
                        }
                        auto initial_suspend() { return suspend_always{}; }
                        auto final_suspend() noexcept { return suspend_always{}; }
                        void return_void() {}
                        void unhandled_exception() {}

                        suspend_always yield_value(int value) {
                            cout << "!!!" << endl;
                            this->value = value;
                            return {};
                        }
                };

                coroutine_handle<promise_type> handler;

                Task3(coroutine_handle<promise_type> handler) : handler(handler) {}
                ~Task3() { this->handler.destroy(); }
        };

        Task1 func1() {
            cout << "  " << __func__ << " " << 1 << endl;

            co_await suspend_always{};

            cout << "  " << __func__ << " " << 2 << endl;

            co_return;
        }

        Task2 func2() {
            cout << "  " << __func__ << " " << 1 << endl;

            co_await suspend_always{};

            cout << "  " << __func__ << " " << 2 << endl;

            co_return 7;
        }

        Task1 func3() {
            cout << "  " << __func__ << " " << 1 << endl;

            co_await suspend_never{};

            cout << "  " << __func__ << " " << 2 << endl;

            co_return;
        }

        Task3 func4() {
            cout << "  " << __func__ << " " << 1 << endl;

            co_yield 7;

            cout << "  " << __func__ << " " << 2 << endl;

            co_return;
        }

        void test_func1() {
            cout << __func__ << " " << 1 << endl;

            Task1 task1 = func1();

            cout << __func__ << " " << 2 << endl;

            task1.handler.resume();

            cout << __func__ << " " << 3 << endl;

            cout << __func__ << " " << 4 << ", done : " << task1.handler.done() << endl;

            task1.handler.resume();

            cout << __func__ << " " << 5 << ", done : " << task1.handler.done() << endl;
        }

        void test_func2() {
            cout << __func__ << " " << 1 << endl;

            Task2 task2 = func2();

            cout << __func__ << " " << 2 << endl;

            cout << __func__ << " " << 3 << ", done : " << task2.handler.done() << endl;

            task2.handler.resume();

            cout << __func__ << " " << 4 << ", done : " << task2.handler.done() << endl;

            cout << __func__ << " " << 5
                 << ", value : " << task2.handler.promise().value << endl;
        }

        void test_func3() {
            cout << __func__ << " " << 1 << endl;

            Task1 task1 = func3();

            cout << __func__ << " " << 2 << endl;

            cout << __func__ << " " << 3 << ", done : " << task1.handler.done() << endl;

            task1.handler.resume();

            cout << __func__ << " " << 4 << ", done : " << task1.handler.done() << endl;
        }

        void test_func4() {
            cout << __func__ << " " << 1 << endl;

            Task3 task3 = func4();

            cout << __func__ << " " << 2
                 << ", value : " << task3.handler.promise().value << endl;

            task3.handler.resume();

            cout << __func__ << " " << 3
                 << ", value : " << task3.handler.promise().value << endl;

            task3.handler.resume();

            cout << __func__ << " " << 4
                 << ", value : " << task3.handler.promise().value << endl;
        }

        int main() {
            test_func1();

            cout << endl << "------" << endl << endl;

            test_func2();

            cout << endl << "------" << endl << endl;

            test_func3();

            cout << endl << "------" << endl << endl;

            test_func4();

            return 0;
        }
  • 실행 결과
        test_func1 1
        ~~~~~~
        test_func1 2
          func1 1
        test_func1 3
        test_func1 4, done : 0
          func1 2
        test_func1 5, done : 1

        ------

        test_func2 1
          func2 1
        test_func2 2
        test_func2 3, done : 0
          func2 2
        test_func2 4, done : 1
        test_func2 5, value : 7

        ------

        test_func3 1
        ~~~~~~
        test_func3 2
        test_func3 3, done : 0
          func3 1
          func3 2
        test_func3 4, done : 1

        ------

        test_func4 1
        test_func4 2, value : 0
          func4 1
        !!!
        test_func4 3, value : 7
          func4 2
        test_func4 4, value : 7
  • custom awaitable
    • 다른 쓰레드에서 재개
    • 코드
        #include <coroutine>
        #include <future>
        #include <iostream>
        #include <memory>
        #include <thread>

        using namespace std;

        class Task {
            public:
                struct promise_type {
                        Task get_return_object() {
                            return Task{
                                coroutine_handle<promise_type>::from_promise(*this)};
                        }
                        auto initial_suspend() { return suspend_always{}; }
                        auto final_suspend() noexcept { return suspend_always{}; }
                        void return_void() {}
                        void unhandled_exception() {}
                };

                coroutine_handle<promise_type> handler;

                ~Task() { this->handler.destroy(); }
        };

        struct awaitable {
                constexpr bool await_ready() const noexcept { return false; }
                void await_suspend(coroutine_handle<> handle) const noexcept {
                    future<void> f = async(launch::async, [handle]() {
                        cout << "    async : " << this_thread::get_id() << endl;
                        handle.resume();
                    });

                    f.get();
                }
                constexpr void await_resume() const noexcept {}
        };

        Task func() {
            cout << "  " << __func__ << " " << 1
                 << ", thread id : " << this_thread::get_id() << endl;

            co_await awaitable{};

            cout << "  " << __func__ << " " << 2
                 << ", thread id : " << this_thread::get_id() << endl;

            co_return;
        }

        void test_func() {
            cout << __func__ << " " << 1 << endl;

            Task task = func();

            cout << __func__ << " " << 2 << ", done : " << task.handler.done() << endl;

            task.handler.resume();

            cout << __func__ << " " << 3 << ", done : " << task.handler.done() << endl;
        }

        int main() {
            test_func();

            return 0;
        }
  • 실행 결과
        test_func 1
        test_func 2, done : 0
          func 1, thread id : 140193857099584
            async : 140193849931328
          func 2, thread id : 140193849931328
        test_func 3, done : 1