asyncio - Python Tulip

주로 성능을 이유로 파이썬에서 비동기 IO가 인기를 얻고 있습니다. GIL (Global Interpreter Lock) 때문에 파이썬에서는 쓰레드를 통한 성능 향상을 꾀하기 어렵다는 점도 이런 경향을 더욱 가속화합니다. 이 영역에서 널리 알려진 이름들에는 Twisted, gevent, Tornado 등이 있습니다. 가장 오랜 역사를 자랑하는 Twisted 는 HTTP 뿐만 아니라 파이썬의 비동기 네트워크 엔진을 추구하고 있고, gevent 는 코루틴(Coroutine)이 콜백(callback)의 대안이 될 수 있음을 보여줬고, Tornado 는 스스로 웹 서버를 표방하고 있지만 Generator 기반의 코루틴 을 사용하는 프레임워크도 포함하고 있습니다. 잘 돌아가고 있는 듯 보이지만 이 들은 서로 호환이 되지 않는다는 문제가 있습니다. Gevent 로 작성한 채팅 서버를 Twisted로 이식하는 것은 처음부터 새로 작성한다고 생각하는 편이 좋습니다. 다른 조합도 상황은 크게 달라지지 않습니다. 한 프레임워크로 작성한 코드를 다른 프레임워크로 옮기는 것이 어렵다는 것이 그리 놀랄만한 일은 아닙니다. 하지만 이 프레임워크들이 공존할 수 있는 방법도 없는 걸까요? 최근 희망이 보이기 시작했습니다.

최근 PEP–3156 이 최종적으로 승인되어 파이썬 3.4 버전에 포함된다는 소식이 올라왔습니다. 이 PEP 의 제안자가 Guido van Rossum 이라는 점도 흥미로운 부분입니다.

asyncio 라는 모듈은 이미 PyPI 에도 등록되어 있고, Google Code 에 Tulip 이라는 코드네임으로 코드가 올라와있습니다.

앞서 알려드린 소식은 이 라이브러리가 Python 3.4부터 기본 내장된다는 뜻입니다. 참고로 asyncio 라이브러리는 Python 3.2 나 그 이전 버전은 지원하지 않습니다. Python 2.7 도 물론 지원하지 않습니다. 개인적으로는 이 것을 마침내 파이썬 3으로 옮겨야 할 피할 수 없는 이유를 얻은 것으로 받아들입니다.

asyncio 를 통해 얻을 수 있는 것은 크게 세가지 입니다.

  • Pluggable Event Loop
  • Transport & Protocol Abstraction
  • Coroutine Scheduler

아직 파이썬 3.4 가 알파에 머무르고 있으니 최종버전이 나올 때는 좀 달라질지도 모르겠습니다만, 현재 까지는 이렇습니다.

앞으로 이어질 포스트들에서 asyncio 의 사용법을 자세히 살펴보기로 하고, 오늘은 예제로 포함되어 있는 fetch0.py 라는 소스코드를 통해 Coroutine 이 어떻게 사용되고 있는지 맛만 보기로 합시다. 혹시 지금부터 나오는 내용을 이해를 못하신다면 다음 포스트를 기다리시는 편이 좋겠습니다.

"""Simplest possible HTTP client."""

import sys

from asyncio import *


@coroutine
def fetch():
    r, w = yield from open_connection('python.org', 80)
    request = 'GET / HTTP/1.0\r\n\r\n'
    print('>', request, file=sys.stderr)
    w.write(request.encode('latin-1'))
    while True:
        line = yield from r.readline()
        line = line.decode('latin-1').rstrip()
        if not line:
            break
        print('<', line, file=sys.stderr)
    print(file=sys.stderr)
    body = yield from r.read()
    return body


def main():
    loop = get_event_loop()
    body = loop.run_until_complete(fetch())
    print(body.decode('latin-1'), end='')


if __name__ == '__main__':
    main()

asyncio 를 사용할 때는 늘 get_event_loop() 로 시작합니다. 이 때 이벤트 루프(Event Loop) 가 만들어지는데, 다른 작업을 위해서는 꼭 필요한 사전 조건입니다. 실제로 이벤트 루프를 실행하는 것은 loop.run_until_complete() 인데, 인자로 fetch() 를 전달하고 있습니다. @coroutine 이라는 데코레이터(Decorator)로 fetch 가 코루틴임을 알려주고 있는데, 이 데코레이터가 꼭 필요한 것은 아닙니다. fetch 를 코루틴으로 만드는 것은 바디에 있는 yield from 이라는 키워드입니다. yield from 도 Python 3.3 에서 새로 도입된 기능인데, asyncio 가 Python 3.3 이전의 버전을 지원하지 않는 주된 이유로 보입니다. fetch()return 하는 bodyloop.run_until_complete() 의 반환값으로 전달되는 것도 역시 yield from 과 함께 도입된 기능입니다. write 를 제외한 IO 앞에 모두 yield from 이 붙어있는 것을 확인하실 수 있을 겁니다.

  • yield from open_connection('python.org', 80)
  • yield from r.readline()
  • yield from r.read()

asyncio 가 코루틴을 통해 실현하려는 스타일은, Synchronous IO 스타일의 코드의 Blocking Call 들 앞에 yield from 을 붙여주는 형태의 수정을 가하는 것입니다. write 앞에 yield from 이 없는 이유는 write 가 Blocking Call 이 아니기 때문입니다.

그럼 끝내기 전에, 보인다는 희망이 무엇인지는 밝혀둬야 할 것 같습니다. Pluggable Event Loop 라는 것인데, 이벤트 루프를 프레임워크가 새로 등록할 수 있을 뿐 아니라, 공존할 수 있는 길을 제공합니다. 앞서 언급한 세 프레임워크들 중 적어도 두 개는 이미 asyncio 지원을 테스트하고 있습니다. 제가 보기에는 나머지 하나도 못할 이유가 없어 보이는군요.