코루틴

흔히 코틀린을 사용할 때 자주 만날 수 있는 단어인 코루틴, 깊게 알아보자.

코루틴이란 Co (ooperation 협동) + routine (루틴) 단어가 합쳐져 생긴 개념이다.

스레드처럼 백그라운드 작업을 위임할 때 많이 사용되는데, 이는 코틀린 뿐만 아니라 여러 언어에서도 통용된다. (Rust 비동기 모델, Go 등등..) 결국 코루틴은 하나의 비동기 프로그래밍을 위한 추상화 도구이기 때문이다.

코루틴이 어떻게 구현되는지 살펴보자. 일반적으로 코루틴은 내부적으로 상태 머신(state machine)을 사용한다.

상태 머신이란 여러 상태(state)를 가지고 있으며, 특정 조건에 따라 상태 간 전환이 이루어지는 시스템을 뜻한다. 코루틴은 실행 중에 여러 상태를 거치며, 각 상태에서 일시 중단되고 다시 시작될 수 있다.

그럼 이 상태 머신은 구체적으로 어떻게 구현될까? 코루틴을 지원하는 언어의 컴파일러는 우리가 작성한 코드를 분석하고 변환하여 내부적으로 상태 머신을 만들어낸다. 예를 들어, 코틀린에서 suspend 키워드가 붙은 함수나 C#의 async/await 구문을 사용하면, 컴파일러는 이 함수들을 여러 조각으로 나누고 각 중단 가능 지점(suspension point)을 기준으로 상태를 관리하는 코드를 생성한다.

코루틴이 일시 중단될 때, 컴파일러는 해당 시점의 **실행 컨텍스트(execution context)**를 저장하기 위한 메커니즘을 작동시킨다. 이 컨텍스트에는 다음 실행을 위해 필요한 모든 정보, 즉 지역 변수의 현재 값들, 명령어 포인터(다음에 실행할 코드의 주소), 그리고 현재 호출 스택의 일부 정보(필요한 경우) 등이 포함된다. 이런 정보들이 힙(heap) 메모리에 할당되는 '컨티뉴에이션 객체(Continuation Object)' 또는 이와 유사한 자료 구조에 패키징되어 저장된다.

나중에 스케줄러(scheduler)에 의해 해당 코루틴이 다시 실행될 준비가 되면 이 저장된 컨티뉴에이션 객체가 활성화된다. 시스템은 이 객체로부터 지역 변수들을 복원하고, 명령어 포인터를 중단됐던 지점으로 정확히 되돌려 놓음으로써 코루틴이 실행을 재개할 수 있게 된다.

상태 머신을 다루는 방법 중에 대표적으로 두가지 방법으로 나뉜다. 스택리스 코루틴과 스택풀 코루틴이다.

스택리스 코루틴

스택리스 코루틴은 상태를 저장함과 동시에 힙으로 변수 값과 실행 위치(몇 번째에서 중단되는지)이 저장된다. 스택을 사용하지 않기 때문에 상대적으로 컨텍스트 스위치 비용도 적고, 메모리 용량도 작아지고 효율적이다.

스택풀 코루틴

스택풀 코루틴은 상태를 저장할 때 스택째로 저장하는 방법이다. 용량도 더 들고, 재귀 함수에서 잘못 사용 시 스택오버플로우가 발생한다. 다만, 일반 함수랑 동일한 원리로 동작하기 때문에 디버깅이 편리하다는 장점이 있다.

시스템 콜

상태 머신은 일반적으로 시스템 콜(syscall)로 제어한다. 거의 코루틴의 핵심이라고 봐도 무방하다.

애플리케이션에서 스레드를 사용하진 않지만, I/O 작업이 발생하면 OS에 해당 작업 스레드를 위임한다. 그리고, 시스템 콜에 해당 작업이 완료되면 이벤트를 보내달라고 요청한다. 그리고 이벤트가 내려지기까지 기다린다. (상태 머신이 상태가 바뀔 때까지 기다림) 이벤트가 내려지면 해당 작업을 다시 재개하는 방식이다.

다음은 운영체제별 메커니즘이다.

리눅스: epoll
윈도우: IOCP
BSD: kqueue

위와 같이 운영체제마다 조금씩 차이가 있지만 동일한 메커니즘으로 이벤트를 내려준다.