프로그램이 실행될 때 불려지도록 만들어진 프로그램의 중심이 되는 코드들을 Main Routine 이라고 하며, Main Routine 이외에 다른 Routine을 모두 SubRoutine 이라고 한다. 그리고 진입 지점을 여러 개 가질 수 있는 SubRoutine을 CoRoutine 이라고 한다. CoRoutine은 호출한 Routine을 대등한 관계로 호출할 수 있기 때문에 다른 Routine의 종속관계가 아니라고 표현하기도 한다. C++ 에서는 Main함수가 Main Routine이고 그 외에 다른 함수들은 모두 SubRoutine 이라고 볼 수 있다. 즉 CoRoutine은 함수 내에서 호출한 쪽을 다시 호출할 수 있고, 다른 Routine에서 함수의 중간 지점을 호출할 수 있는 것이라고 할 수 있다. 참조(출처) : https://gamedevforever.com/291 / https://yizhang82.dev/cpp-coroutines-async-fibonacci |
보통 Coroutine을 Thread와 비슷한 개념이라고 말하기도 하는데, 저는 이것이 Coroutine을 이해하기 어렵게 만든다고 생각합니다. 결과적으로 보면 비슷한 면이 많습니다. Thread와 Coroutine 모두 자신만의 스택이 존재하고 실행 순서를 가집니다. 하지만 Coroutine은 Thread가 아닙니다.
Thread란 프로그램 내에서 실행되는 흐름의 단위를 말합니다. 모든 프로그램은 최소 하나의 Thread를 가지며, 이 Thread를 Main Thread(주 스레드)라고 합니다. 그리고 이 Main Thread에서 Main Routine이 불려집니다. Thread는 흐름의 단위이기 때문에 새로운 Thread가 만들어졌다는 것은 새로운 시간 흐름이 만들어졌다고도 볼 수 있습니다. 이렇게 프로그램은 여러 개의 Thread를 동시에 실행할 수도 있고, 이것으로 인해 일종의 흐름이 동시에 진행될 수 있습니다. 이러한 실행 방식을 Multithread(멀티스레드)라고 합니다.
C++20 에서 함수에 이러한 키워드가 발생하면 코루틴이 됩니다.
이전 버전과의 호환성을 위해, 키워드의 시작부분에 co_가 붙었습니다. ( 기존에 있던 키워드 )
코루틴 시작
만약 우리가 코루틴을 가제기 되면, 그것과 소통할 수 있는 어떠한 방법이 있어야 합니다. ( 중단, 재개 )
그래서 코루틴의 return 타입을 통해 코루틴과 통신하게 됩니다.
만약 우리 코루틴이 중단될 수 있다면, 우리는 재개(Resumable)할 수도 있어야 합니다.
그래서 우리의 return 타입은 resume 타입이 필요합니다.
만약 아직 실행되지 않았다면, 재개(resume) 할 것이고, resume 함수를 통해 그 안에 여전히 실행될 것이 있는지를 나타내게 할 것입니다.
따라서 다음과 같이 Coroutine의 첫 함수를 작성합니다.
suspend_always 객체는 일반적인 co_await 의 결과 입니다.
실제로 이 함수는 적어보이는데, 컴파일러가 몇 가지 추가 코드를 추가하게 됩니다.
모든 코루틴 기능은 컴파일러에 의해 다음과 유사한 형태로 변환됩니다.
일단 컴파일을 해봅시다. 다음과 같은 에러가 발생합니다.
이는 위에서 컴파일러가 추가한 소스와 동일하게 어떠한 정보(promise_type)를 우리가 작성해 주어야 한다는 것입니다.
우리는 두가지 방법으로 할 수 있습니다.
1. resumable type 의 멤버로 promise_type ( 또는 동일한 이름의 별칭 ) 생성
2. coroutine_traits 를 사용하여 promise_type을 정의 ( 또는 동일한 이름의 별칭 생성 )
우리는 1번 방법으로 할 것이며, coroutine_traits 의 정보는 다음에 있습니다.
https://en.cppreference.com/w/cpp/language/coroutines
만약 우리의 코루틴이 아무것도 반환하지 않고 ( co_return 을 통해서 ) 코루틴을 흐른다면, 우리는 추가적인 멤버 함수를 정의 하여야 합니다.
이렇게 만들어진 resumable type의 초안입니다.
아직 promise_type 의 함수에 어떤 정의도 넣어놓지 않았습니다.
코루틴을 사용하기 위해서는 코루틴에 대한 일종의 핸들이 필요합니다. 우리가 관리할 것 입니다.
코루틴을 위한 기본 제공 객체가 이미 있습니다.
coroutine_handle은 Coroutine의 동적 할당 상태를 나타냅니다. 이 객체를 이용해 코루틴을 관리할 수 있습니다.
cotorutine_handle은 템플릿 형식이며, 여기서 Promise유형은 템플릿 인수입니다.
coroutine_handle은 default 생성이 가능합니다. ( 위의 nullptr_t 생성자 )
비어 있음은 operator bool 을 사용하거나 address 함수를 통해 nullptr과 비교하여 확인할 수 있습니다.
[ 참고 : operator bool 은 코루틴이 완료됐는지? 에 대한 결과와 혼동될 수 있습니다. ]
만약 비어 있지 않은 코루틴을 만드려면 from_address 라는 정적 멤버 함수를 사용해야 합니다.
코루틴은 다음에 의해 소멸자가 호출 됩니다.
1. coroutine_handle 유형의 destroy 멤버 함수 호출
2. (최종 정지 지점 이후) 함수의 흐름이 코루틴 기능을 상실할 때
코루틴은 여러분 중단될 수 있습니다. 최종 정지 지점에서 코루틴이 중단된 경우 2번 방법이 true가 됩니다.
이를 통해 실제로 resumable을 작성하겠습니다.
resumable은 copyable 하게 하고싶지 않습니다. destroy 기능을 한 번만 호출할 수 있기 때문에 적합하지 않습니다.
resumeable의 흐름은 다음과 같습니다 :
" 만약 우리의 코루틴이 아직 끝나지 않았다면, 재개(resume) 하고 그렇지 않다면 재개하지 마세요. "
반환되는 값은 재개 요청 후에 다시 coroutine을 계속할 수 있는지 여부를 알려줍니다.
아직 기능이 다 갖춰지지 않았습니다. done 요청의 기능이 갖춰지지 않았습니다.
올바르게 작동하려면 최종 일시 중단 지점에서 Coroutine이 스스로 중단되어야 합니다.
coroutine_handle 객체는 스레드세이프 하지 않습니다. 예를 들어 destroy 또는 resume에 동시에 접근하면 data race가 발생할 수 있습니다. destroy 함수의 복수 호출은 동일한 포인터에 파괴 요청을 두번 하는 셈이므로, 주의하여야 합니다. [ 참고 : 코루틴은 최종 중단 지점에 도달한 후에 스스로 파괴 합니다. ] |
이제 promise_type을 정의하여 co_routine이 동작하도록 해야 합니다.
일단 get_return_object를 호출하여 resumable type을 생성합니다.
resumable type은 promise_type에서 생성한 coroutine_handle 이 필요합니다.
그 다음, initial_suspend가 호출됩니다.
처음 초기화 작업에서 중단, 재개에 대한 여부는 차이가 없으므로 suspend_always를 사용합니다.
final_suspend는 coroutine_handle의 완료가 적절하게 됐을때만 suspend_always를 반환해야 합니다.
완성된 resumable 클래스는 다음와 같습니다.
'C++ > Modern' 카테고리의 다른 글
C++20) Coroutine ( 코루틴 ) - 3 (0) | 2020.08.23 |
---|---|
C++20) Coroutine ( 코루틴 ) - 2 (0) | 2020.08.21 |
C++20) Designated Initializer ( 지정된 초기화 ) (0) | 2020.08.19 |
C++17) std::variant (0) | 2019.02.28 |
C++17) std::any (0) | 2019.02.27 |