coroutine — 코루틴 만들고 값 생산하기
coroutine — 코루틴 만들고 값 생산하기
Tcl에서 여러 작업을 한 프로그램 안에서 동시에 진행하고 싶을 때가 있어요. 그럴 때 유용한 도구가 바로 coroutine이에요. 함수처럼 썼다가 중간에 멈추고, 다시 불러서 이어갈 수 있게 해 주는 개념이죠. 이걸 이용하면 하나의 스레드 안에서 값 스트림을 만들거나, 여러 코루틴이 서로 값을 주고받는 구조를 쉽게 짤 수 있어요.
본문
시그니처
coroutine name command ?arg...?
yield ?value?
yieldto command ?arg...?
name ?value...?
설명
coroutine 명령은 name이라는 새 코루틴 컨텍스트(그에 딸린 명령 포함)를 만들고, 그 컨텍스트 안에서 command를 나머지 인자와 함께 별다른 해석 없이 호출해 실행해요. command가 정상적으로 반환하거나 예외(예: 에러)로 끝나면 코루틴 컨텍스트 name은 삭제돼요.
컨텍스트 안에서는 yield 명령으로 값을 결과로 생산할 수 있어요. 값을 지정하지 않으면 빈 문자열이 사용돼요. yield가 호출되면 컨텍스트는 실행을 멈추고, coroutine 명령은 yield에 넘겨진 인자를 반환해요. 그러면 컨텍스트 명령을 호출해 실행을 재개할 수 있는데, 이때 멈추게 만든 yield 호출의 결과로 쓸 값을 하나 넘길 수 있어요.
코루틴이 yield 없이 일반적인 반환을 한다면, coroutine 명령의 결과는 컨텍스트를 평가한 결과가 돼요.
코루틴은 yieldto 명령으로도 실행을 멈출 수 있어요. 이때는 반환 대신 실행을 어떤 명령 command(코루틴의 컨텍스트에서 해석됨)에 넘기는데, 인자는 몇 개든 전달할 수 있어요. 모든 코루틴에는 컨텍스트 명령이 있으므로 yieldto를 쓰면 한 코루틴에서 다른 코루틴으로 직접 제어를 넘길 수 있어요(두 코루틴이 이를 예상하고 있을 때만 권장돼요). 어떤 명령이든 대상이 될 수 있어요. 이 방식으로 코루틴이 멈추면, 컨텍스트 명령을 호출해 임의 개수의 인자를 넘기며 재개할 수 있어요. yieldto 호출의 반환값은 컨텍스트 명령에 넘겨진 인자들의 목록이에요. 그 값들을 어떻게 처리할지는 호출자가 정하면 돼요.
여러 인자로 재개를 허용하는 yield 버전을 만드는 권장 방법은 yieldto와 return을 함께 쓰는 거예요:
proc yieldm {value} {
yieldto return -level 0 $value
}
코루틴은 명령 이름을 파괴해서도 삭제할 수 있고, 현재 코루틴의 이름은 info coroutine으로 얻을 수 있어요. 코루틴 구현에 있는 변수에 삭제 트레이스가 있으면 코루틴이 명시적으로 삭제될 때(또는 자연스럽게 일반 반환할 때) 그 트레이스가 발동돼요.
command가 호출되는 시점에 현재 네임스페이스는 전역 네임스페이스이고 그 위에는 스택 프레임이 없어요(upvar·uplevel의 관점에서). 다만 어떤 명령을 호출할지는 coroutine 명령이 호출된 네임스페이스에서 결정돼요.
예제
짝수 값을 무한히 생산하는 코루틴과, 그중 처음 10개를 소비하는 루프예요:
proc allNumbers {} {
yield
set i 0
while 1 {
yield $i
incr i 2
}
}
coroutine nextNumber allNumbers
for {set i 0} {$i < 10} {incr i} {
puts "received [nextNumber]"
}
rename nextNumber {}
코루틴이 자신에게 넘겨지는 인자를 더해가는 예제예요:
coroutine accumulator apply {{} {
set x 0
while 1 {
incr x [yield $x]
}
}}
for {set i 0} {$i < 10} {incr i} {
puts "$i -> [accumulator $i]"
}
에라토스테네스의 체로 소수를 찾는 고전 알고리즘을 코루틴으로 구현한 예제예요. 코루틴 안에서 코루틴을 만들고 있는 점에 주목하세요.
proc filterByFactor {source n} {
yield [info coroutine]
while 1 {
set x [$source]
if {$x % $n} {
yield $x
}
}
}
coroutine allNumbers apply {{} {while 1 {yield [incr x]}}}
coroutine eratosthenes apply {c {
yield
while 1 {
set n [$c]
yield $n
set c [coroutine prime$n filterByFactor $c $n]
}
}} allNumbers
for {set i 1} {$i <= 20} {incr i} {
puts "prime#$i = [eratosthenes]"
}
세 코루틴이 서로에게 값을 주고받으며 넘기는 예제예요:
proc juggler {name target {value ""}} {
if {$value eq ""} {
set value [yield [info coroutine]]
}
while {$value ne ""} {
puts "$name : $value"
set value [string range $value 0 end-1]
lassign [yieldto $target $value] value
}
}
coroutine j1 juggler Larry [
coroutine j2 juggler Curly [
coroutine j3 juggler Moe j1]] "Nyuck!Nyuck!Nyuck!"
상세 의미론
이 예제는 코루틴이 전역 네임스페이스에서 시작된다는 점과, 명령 해석이 코루틴 스택이 만들어지기 전에 일어난다는 점을 보여줘요.
proc report {where level} {
# Where was the caller called from?
set ns [uplevel 2 {namespace current}]
yield "made $where $level context=$ns name=[info coroutine]"
}
proc example {} {
report outer [info level]
}
namespace eval demo {
proc example {} {
report inner [info level]
}
proc makeExample {} {
puts "making from [info level]"
puts [coroutine coroEg example]
}
makeExample
}
다음 출력이 나와요. 특히 스택 조작이 일어났고(첫째·둘째 줄의 레벨 비교), 코루틴의 부모 레벨이 전역 네임스페이스라는 걸 알 수 있어요. 또한 코루틴 이름은 지정하지 않으면 현재 네임스페이스에 한정되며, 코루틴은 호출된 프로시저 안 같은 깊은 곳에서도 yield할 수 있다는 것도 확인할 수 있어요.
making from 2
made inner 1 context=:: name=::demo::coroEg
더 알아보기
apply,info,proc,return- 키워드:
coroutine,generator