살아가는 이야기
Python에서 코루틴 구현하기(coroutines in Python) 본문
Simula 67에는 코루틴(coroutine)이라는 서브프로그램이 있다. Simula는 클래스 개념으로도 유명한 언어인데 60년대 언어다 보니 지금 사용해 볼 수 없고, 따라서 코루틴이 무엇인지 제대로 알 수 있는 참고자료가 드물다.
그런데 코루틴과 유사한 제너레이터(generator)를 Python에서 지원하고 있다. 따라서 Python의 제너레이터를 이용하면 코루틴을 시뮬레이션할 수 있다. 이 글은 Python의 제너레이터를 이용하여 코루틴을 구현하는 방법에 관한 글이다. (사실 구글로 검색하면 코루틴에 관한 글이 많이 있지만 너무 복잡하여 거의 알아볼 수 없었다.)
코루틴은 서브프로그램이지만 글자 그대로 상호 협력하는 루틴(co-routine)이다. 따라서 호출자와 피호출자의 개념이 없고 서브프로그램 자신의 상태를 그대로 지니게 된다. 보통은 서브프로그램 sub1
이 sub2
를 호출하면 sub2
가 종료된 후에 sub1
의 수행이 계속되지만, 코루틴의 경우에는 sub2
가 제어를 sub1
으로 넘겨도 sub2
의 상태는 보존된다. 따라서 다음에 sub2
가 재개되면 지난 번 수행했던 다음부터 수행된다. 그래서 코루틴에서는 다른 프로그램을 호출(call)한다고 하지 않고 재개(resume)한다고 한다.
다음 의사코드(pseudocode)는 위키피디아(http://en.wikipedia.org/wiki/Coroutine)에 소개된 코루틴 코드다. 같은 큐(queue
)를 공유하며 데이터를 생성하고 소비하는 코루틴이다.
var q := new queue
coroutine produce
loop
while q is not full
create some new items
add the items to q
yield to consume // consume을 재개함
coroutine consume
loop
while q is not empty
remove some items from q
use the items
yield to produce // produce를 재개함
이와 유사한 코드를 Python으로 작성해 보았다. 코드를 간단히 하기 위해 큐 대신 크기 1인 버퍼를 사용하였다. 생산자(producer)는 1, 2, 3, ...을 생성하고 소비자(consumer)는 이를 제곱하여 출력한다.
class Buffer:
def __init__(self):
self.written = False
self.data = 0
def full(self):
return self.written
def empty(self):
return not self.written
def add(self, item):
if self.full():
return None
else:
self.data = item
self.written = True
return self.data
def remove(self):
if self.empty():
return None
else:
self.written = False
return self.data
def producer():
item = 0
while True:
if not buf.full():
item += 1
buf.add(item)
yield 'consumer'
def consumer():
while True:
if not buf.empty():
item = buf.remove()
print(item * item, end=" ")
yield 'producer'
if __name__ == '__main__':
buf = Buffer()
prod = producer()
cons = consumer()
dTab = {'producer':prod, 'consumer':cons} # dispatch table
curr = prod
for n in range(20):
curr = dTab[next(curr)]
코드의 상당 부분을 차지하는 Buffer
(처음부터 21행까지)는 크기 1인 버퍼를 구현한 코드이다. 실제 중요한 부분은 코루틴과 이를 구동하기 위한 호출 테이블(dispatch table) dTab
이다.
Python의 yield
는 제너레이터에서 값을 호출자에게 반환하는 것이므로 생산자에서 직접 소비자를 부를 수 없다. 직거래가 불가능한 것이다. 그래서 위 프로그램에서는 호출 테이블 dTab을 이용하고 있다. 생산자 코루틴이 yield
를 통해 호출자에게 제어를 넘기면 호출자가 다시 생산자가 지정한 코루틴을 호출하도록 하고 있다. 위 프로그램의 실행 결과는 다음과 같다.
1 4 9 16 25 36 49 64 81 100
curr
변수가 현재 수행할 코루틴을 가리키고 있으며 코루틴 수행이 끝나면 next(curr)
를 dTab
에서 찾아서 다음에 수행할 코루틴을 결정한다.
코루틴이 이전에 제어를 넘긴 바로 다음 지점부터 수행되는지 파악하려면 생산자와 소비자의 yield
다음에 print
를 이용하여 'producer'
, 'consumer'
등을 출력하도록 코드를 수정하면 이를 확인할 수 있다.