코루틴을 구분해보자

Jooyung Han (한주영)
8 min readDec 10, 2017

--

이전 글: 코루틴 소개

지난 글 끝에 Generator 코루틴이나 Async 코루틴 등이 Asymmetric 코루틴이고, 반대 개념인 Symmetric 코루틴이란 것도 있다고 했다.

코루틴을 구분하는 다른 기준도 있다. 관련 내용이 Revisiting Coroutines 2004 라는 페이퍼에 잘 설명되어 있다. 여기 나온 몇가지 기준들을 살펴보면 코루틴을 좀 더 잘 이해할 수 있을 것 같다.

이 페이퍼는 Lua 언어를 만든 호베르투 이에루잘림스시(Roberto Ierusalimschy) 교수가 Lua의 코루틴을 변명(?)하기 위해 쓴 것 같다.

(여기는 소설..)뭔고하니… 코루틴이 주목받지 못하던 상황에 Lua에서 코루틴을 지원했는데 공격을 많이 받은 모양이다. 코루틴이 제대로 정리되지도 않은 상황에 다들 저마다의 기준을 가지고 Lua의 코루틴을 “반쪽짜리" 코루틴으로 취급했으니… 그렇지 않다, 라고 변명아닌 변명을 한 것이 아닌가 한다.

암튼, 그래서 이 페이퍼가 말하는 바는 대충 이렇다.

  • 코루틴의 시맨틱을 정확히 기술한다.
  • 코루틴 동작의 다양성을 1) 제어권 전달 방식(symmetric/asymmetric) 2) 코루틴의 일급 객체인지(first-class) 3) 콜스택이 쌓인 경우도 지원하는지(stackful) 이렇게 세가지 기준으로 구분한다.
  • 2번 3번을 만족한다면 완전한(Full) 코루틴이라고 부를 수 있다.
  • 이렇게 봤을 때, Lua 코루틴은 “비대칭(Asymmetric) 완전 코루틴".
  • 그런데, 많은 사람들의 오해와 달리 비대칭 코루틴은 대칭 코루틴에 비해 표현력이 떨어지지 않으며 오히려 사용하기 더 쉽다.

즉, 기존의 “반쪽" 개념을 대칭/비대칭의 문제로 바꿔놓으면서 오히려 일급객체로 지원하며 콜스택을 지원하는 경우를 “완전"한 코루틴이라고 관점을 바꿀 것을 제안한다.

코루틴의 최소 정의

코루틴은 “루틴" 상의 로컬 상태를 유지하면서 제어를 반환했다가(suspend), 제어권을 다시 획득했을 때(resume) 흐름을 이어갈 수 있다.

대칭/비대칭

코루틴이 멈출 때 제어권을 넘겨받을 다른 코루틴을 지정하는 방식이 “대칭" 코루틴이며, 코루틴이 멈추면 자동으로 코루틴에 제어권을 주었던 지점으로 되돌아 가는 방식이 “비대칭" 코루틴이다.

비대칭 코루틴은 제어권의 흐름이 대칭 코루틴에 비해 더 “구조적" 프로그래밍에 가까우며, 이 때문에 이해하고 사용하기 더 쉽다.

그런데 비대칭 코루틴을 이용하면 대칭 코루틴을 쉽게 만들 수 있다. Lua는 대칭 코루틴을 위한 인터페이스를 라이브러리 형태로 제공한다.

위키피디어에도 대칭 코루틴을 이용하여 Producer-consumer를 구현한 뒤, 비대칭 코루틴에 Dispatch loop를 더해 대칭 코루틴을 모사할 수 있음을 보여준다.

다음은 대칭 코루틴 예제. (pseudocode)

var q := new queuecoroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce

다음은 Generator를 이용하여 앞의 대칭 코루틴을 흉내낸 것. Generator는 비대칭 코루틴이니 값을 yield할 때 Caller에게 제어권이 넘어간다. 그런데 Generator가 일급 객체라면 yield 대상이 될 수 있다. 그러면 Caller는 Generator로부터 넘겨받은 다른 Generator를 next()로 resume해 주기만 하면 대칭 코루틴을 모델링 할 수 있다.

var q := new queuegenerator produce
loop
while q is not full
create some new items
add the items to q
yield consume
generator consume
loop
while q is not empty
remove some items from q
use the items
yield produce
subroutine dispatcher
var d := new dictionary(generator → iterator)
d[produce] := start produce
d[consume] := start consume
var current := produce
loop
current := next d[current]

완전 코루틴

대칭이냐 비대칭이냐 보다 중요한 것이 일급 객체인지, 스택을 지원하는지 라고 이 페이퍼가 주장 아닌 주장을 하는데, 이렇게 보면 대부분의 언어들이 그렇지 못하다.

Generator는 일급 객체

대부분의 Generator는 Iterator라는 구체적인 인터페이스가 있기 때문에 마치 일급 객체인 코루틴처럼 사용할 수 있다. next()로 해당 코루틴을 resume할 수 있기 때문이다. (하지만 Async 코루틴은 그렇지 못하다. 그래서 Generator 코루틴만 있다면 Async 코루틴을 비롯하여 여러가지 다양한 구현에 응용할 수 있다.)

유니티도 C#/JS의 Generator를 일찍부터 ‘코루틴'이란 이름으로 동시성 구현에 사용하고 있다.

JavaScript에도 Generator를 이용하여 Async/await 처럼 비동기 프로그래밍을 지원하는 “co”라는 라이브러리를 비롯하여, “redux-saga”라는 라이브러리 역시 Generator를 이용하여 백그라운드 프로세싱을 콜백 문법 대신 direct-style로 지원한다. “js-csp”라는 라이브러리는 Go언어의 동시성 모델인 Communicating Sequential Processes를 지원하는데 이는 Generator 덕분에 가능한 것이다.

예를 들어 “co” 라이브러리를 조금 살펴보면.. 이 라이브러리는 async/await 문법을 generator로 구현하고 있다. (흥미롭게도 이 라이브러리는 README에서 코루틴을 언급조차 하지 않는다. 물론 라이브러리 이름만으로 코루틴 라이브러리라는 걸 알 수 있다.) Async 코루틴이니까 Generator 함수에서 yield 하는 값이 Promise고, 최종 결과까지 run-through 해 줄 dispatch loop에 해당하는 코드가 있을 것이라는 것도 짐작할 수 있다. 이 경우는 symmetric 코루틴을 구현한 dispatch loop의 동기적 루프와는 달리 Promise.then에 콜백 형태인 “비동기 루프”로 구현할 수 있을 것이다. (아래는 약식으로 구현한 “co”의 dispatch loop 코드)

function co(gen) {
return new Promise(function (resolve) {
onFulfilled();
function onFulfilled(res) {
const {value, done} = gen.next(res);
if (done) resolve(value);
else value.then(onFulfilled);
}
}
}

콜스택 지원

대부분의 언어에서 제공하는 Generator는 일급 객체이긴 해도 콜스택을 지원하지 않는다. 여기서 콜스택 지원 여부는 현재 진행 중인 코루틴을 nested call에서도 멈출 수 있느냐로 결정된다. 대개는 yield 키워드를 현재 코루틴 문맥에서만 지원한다. 그 이유는 많은 코루틴 구현이 코루틴만의 별도 콜스택을 만들지 않고 기존의 함수 호출 메커니즘을 최대한 활용한 컴파일러 기술(혹은 매크로)에 의존하기 때문이다.

function *g() {
nested();
}
function nested() {
yield; // not allowed
}

Stackful 하지 않은(Stackless) 코루틴만 지원되는 경우, 대개는 코루틴 중첩이 필요하다.

function* g() {
yield* nested();
}
function* nested() {
yield;
}

일반적인 함수호출 문법을 벗어나기 때문에 편리하진 않아도 불가능하진 않다. 좋은 호출 인터페이스를 제공하고 싶은 라이브러리 개발자 입장에서만 좀 아쉬운 정도? (추가 설명은 Stackful/Stackless 코루틴 참고)

어떤 언어/라이브러리가 코루틴을 지원한다고 하면 여기서 이야기한 symmetric/asymmetric, first-class or not, stackful/stackless 정도를 따져보는 게 이해하는데 도움이 될 듯하다. 적잖은 문서들이 직접적으로 이 용어를 사용하여 자기네 코루틴이 어떤 속성을 가지는지 설명한다.

다음 글: 코루틴과 파이버

--

--

Jooyung Han (한주영)

가끔 함수형 프로그래밍 관련 글을 쓰거나 번역합니다. “개미 수열을 푸는 10가지 방법"이란 책을 썼습니다. https://leanpub.com/programming-look-and-say