살아가는 이야기

Python에서 코루틴 구현하기(coroutines in Python) 본문

컴퓨터, 풀어그림

Python에서 코루틴 구현하기(coroutines in Python)

우균 2014. 5. 28. 17:52

Simula 67에는 코루틴(coroutine)이라는 서브프로그램이 있다. Simula는 클래스 개념으로도 유명한 언어인데 60년대 언어다 보니 지금 사용해 볼 수 없고, 따라서 코루틴이 무엇인지 제대로 알 수 있는 참고자료가 드물다.

그런데 코루틴과 유사한 제너레이터(generator)를 Python에서 지원하고 있다. 따라서 Python의 제너레이터를 이용하면 코루틴을 시뮬레이션할 수 있다. 이 글은 Python의 제너레이터를 이용하여 코루틴을 구현하는 방법에 관한 글이다. (사실 구글로 검색하면 코루틴에 관한 글이 많이 있지만 너무 복잡하여 거의 알아볼 수 없었다.)

코루틴은 서브프로그램이지만 글자 그대로 상호 협력하는 루틴(co-routine)이다. 따라서 호출자와 피호출자의 개념이 없고 서브프로그램 자신의 상태를 그대로 지니게 된다. 보통은 서브프로그램 sub1sub2를 호출하면 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' 등을 출력하도록 코드를 수정하면 이를 확인할 수 있다.

Comments