반응형

해당 본문은 파이썬에서의 비동기 프로그래밍의 흐름에 대한 글입니다.
실제 비동기 프로그래밍을 위한 asyncio 사용법 정보는 맨 밑에 추가 link 참고 부탁드립니다.

0. 배경

멀티 프로세스 환경이 아닌 하나의 CPU를 사용하는 것을 가정한다. 그렇다면 프로그램은 각 라인별로 실행될 것이다.

즉 원격 서버에 접속을 하는 등의 코드 라인은, 서버 연결이 완료될 때 까지 프로그램이 아무것도 하지 못한다.

1. 스레딩(threading)

이를 해결하기 위한 방법이 스레딩이다. 프로그램은 여러개의 스레딩을 돌릴 수 있고, 각 스레드는 동시에 다른 작업들을 수행한다. 하지만 다중 스레드 프로그램은 복잡하고, race conditions, dead-locks, live-locks, resource-starvation 등을 포함한 까다로운 에러가 발생하기 쉽다.
위에 각 스레드가 동시에 다른 작업을 수행한다고 했지만, 엄밀하게는 각 CPU 코어는 한 번에 하나의 스레드만 돌릴 수 있다. 그래서 마치 여러개의 스레드가 동시에 돌아가는것 처럼 보이게 하기 위해 컨텍스트 스위칭이 자주 일어난다.
이것을 단순화하기 위해 CPU는 임의의 간격으로 스레드의 모든 컨텍스트 정보를 저장하고, 다른 스레드로 전환한다.
컨텍스트 스위칭 또한 자원이며 공짜가 아니다. 또한 컨텍스트 스위칭은 각 스레드의 작업을 이해하지 못하기 때문에, 불필요한 컨텍스트 스위칭이 일어날 수 있다.

예를들면, 5개의 스레드중, 2개의 스레드는 동작중이고, 3개의 스레드는 다른 작업을 대기중이다. 작업을 대기중이 3개의 스레드에게도 컨텍스트 스위칭이 이루어지며, 이때 스레드는 할 일이 없기 때문에 바로 sleep 으로 들어가고 다시 컨텍스트 스위칭이 이루어진다.

2. 그린 스레드 (Green Threads)를 이용한 비동기

파이썬에서는 비동기를 구현하기 위해 그린 스데르 가 등장하였다.
그린스레드란

  • 스케쥴링을 하드웨어가 아닌 애플리케이션 코드가 대신 한다.
  • 이를 제외하고는 일반 스레드와는 차이가 없다
    python에서는 Gevent가 있는데, 그린 스레드 + eventlet (non-blocking I/O 네트워킹 라이브러리)
import gevent
from gevent import monkey
from urllib.request import urlopen

monkey.patch_all()
urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def print_head(url):
    print('Starting {}'.format(url))
    data = urlopen(url).read()
    print('{}: {} bytes: {}'.format(url, len(data), data))

    jobs = [gevent.spawn(print_head, _url) for _url in urls]
    # jobs 가 이벤트 루프
    # jobs 리스트 안의 오브젝트들이 코루틴
    gevent.wait(jobs)
  • 스레딩과 유사해보이지만, 실제 구현은 코루틴을 사용하며, 스케쥴링을 위해서 이벤트 루프 위에서 코루틴을 실행한다.
  • 코루틴에 대한 이해 없이 경량 스레딩을 사용할 수 있지만, 스레딩의 문제점이 해결되지는 않았다.
  • Gevent는 스레딩을 이미 이해하고 있고, 경량 스레딩을 사용하고자 할 때 좋은 라이브러리이다.

이벤트 루프

이벤트/잡 등을 관리하는 큐가 있을 때, 큐에서 지속적으로 이벤트/잡을 꺼내고 실행하는 루프이다.

코루틴

이벤트 루프가 접근하는 큐에 넣을 수 있는 이벤트를 포함하는 명령어들의 작은 집합이다
즉 위의 이벤트 루프에서 이벤트/잡 들을 코루틴이라고 부른다
이벤트 루프에 넣은 코루틴들을 파이썬에서는 리스트에 넣은 함수 호출이라고 생각하면 될것같다.

3. 콜백을 이용한 비동기

콜백은 함수이며, "특정 작업이 완료되었을 떄 이 함수를 실행시켜줘" 라는 의미를 가진다.
python 에서는 Tornado 라이브러리가 있다.

  • Tornado는 비동기 I/O를 위해 콜백 스타일을 사용하는 비동기 웹 프레임워크이다.
# 아래 예시 코드는 Tornado 라이브러리가 패치되면서 현재는 실행되지 않는 코드입니다.  
# 콜백의 예시로 봐주시면 되겠습니다.
from tornado.httpclient import AsyncHTTPClient  
urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

def handle\_response(response):
    # response 가 오면 함수(콜백)가 실행됨
    if response.error:  
        print("Error:", response.error)  
    else:  
        url = response.request.url  
    data = response.body  
    print('{}: {} bytes: {}'.format(url, len(data), data))

http_client = AsyncHTTPClient()  
for url in urls:  
    http_client.fetch(url, handle_response)
  • fetch 실행시, 바로 리턴되고 다음 코드가 실행된다.
  • fetch 완료 시 리턴 객체를 얻을 수 없기 때문에, 콜백 함수를 통하여 가져온다.

콜백 지옥

위의 Tornado 예시코드에서 보면 콜백 함수 내부에서 에러 확인을 한다. 콜백에서 예외가 발생한다면 이벤트 루프로 전달되지 않기 때문에, 에러는 반드시 객체로 전달되어야 한다.
블로킹을 없에는 일반적인 방법은 콜백밖에 없다. 그렇기 때문에 때로는 콜백의 콜백의 콜백같은 긴 콜백 체인이 만들어지고, 필요한 변수와 에러들을 모든 콜백에 큰 객체로 밀어넣게된다.

4. 파이썬의 비동기 정리

그린 스레드 스타일

  • 스레드들이 하드웨어 대신 애플리케이션 레벨에서 관리됨
  • 일반 스레드와 유사함 : 스레드를 잘 아는 사람들이 쓰기 좋음
  • 컨텍스트 스위칭 문제를 제외하고 스레드 프로그램에서 발생하는 모든 문제점들을 갖고있음

콜백 스타일

  • 프로그래머는 스레드와 코루틴을 직접 볼 수 없음
  • 콜백은 예외를 발생시키지 않으며 수집할 수 없음
    • 따라서 디버깅이 어려움

5. 파이썬 3.4 부터의 비동기

위의 방법(그린 스레드, 콜백)이 파이썬 3.3 까지는 최선의 방법이었다. 더 나은 방법을 위해서는 언어 자체의 지원이 필요했다.
즉, 메서드를 부분적으로 실행시키고, 실행을 중단시키고, 스택객체와 예외를 전역적으로 관리할 수 있는 방법이 필요하다.

파이썬 제너레이터가 힌트가 될 수 있다.

파이썬 제너레이터

리스트를 리턴하는 제너레이터는 ,한번에 하나의 아이템만 리턴하며, 다음 아이템이 필요할 때 까지 중지된다.
제너레이터의 문제점은, 함수가 제너레이터를 호출해야지만 수행이 된다, 제너레이터는 제너레이터를 호출할 수 없고, 서로의 실행을 중지할 수 없다.

하지만 PEP 380 에서 제너레이터가 다른 제너레이터의 결과값을 yield 할 수 있게 해주는 yield from 문법이 추가되면서 이야기가 달라진다. 비동기를 지원하기 위해 제너레이터를 만든것은 아니지만, 제너레이터는 비동기가 잘 돌아가는데 필요한 모든 기능을 제공한다. 제너레이터는 스택을 유지하며 예외를 발생시킬 수 있다. 제너레이터를 코루틴으로 실행하는 이벤트 루프를 작성한다면, 훌륭한 비동기 작업을 수행할 수 있을 것이다

그리하여 asyncio 라이브러리가 탄생하였다.

파이썬 asyncio + 제너레이터

@coroutine 데코레이터를 추가하면 asyncio는 제너레이터를 코루틴 안으로 넣을 것이다.

import asyncio  
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

@asyncio.coroutine  
def call_url(url):  
    print('Starting {}'.format(url))  
    response = yield from aiohttp.ClientSession().get(url)  
    data = yield from response.text()  
    print('{}: {} bytes: {}'.format(url, len(data), data))  
    return data

futures = [call_url(url) for url in urls]

loop = asyncio.get_event_loop()  
loop.run_until_complete(asyncio.wait(futures))
  • 에러 처리를 따로 하지 않았다, 왜냐하면 에러는 스택으로 제대로 전달된다
  • 객체를 리턴하고 받을 수 있다.
  • 모든 코루틴들을 시작시키고, 나중에 결과를 받을 수 있다.
  • 콜백을 사용하지 않는다.
  • 첫번째 yield from 이 끝나야 두번째 yield from 이 실행된다
    • 이 예제는 yield from을 사용하여 완전하게 비동기로 동작하는건 아닌거 같다

마지막 남은 이슈는 yield from 문법 때문에 실제로는 함수가 아니라 제너레이터이다.
(예시에서 futures 객체를 열어보면 안에 제너레이터 객체가 있다)

6. 파이썬 3.5 부터 asyncio + async/await

asyncio 가 파이썬 코어라이브러리가 되면서 async 와 await 키워드가 추가되어, 비동기적으로 동작하는걸 명확하게 알 수 있게 되었다.

  • async 키워드는 메서드가 비동기임을 알 수 있도록 def 앞에 위치한다
  • await 키워드는 위의 yield from 을 대신하며 코루틴이 끝날때 까지 대기하고 있음을 명확하게 알 수 있다.
import asyncio  
import aiohttp

urls = ['http://www.google.com', 'http://www.yandex.ru', 'http://www.python.org']

async def call_url(url):  
    print('Starting {}'.format(url))  
    response = await aiohttp.ClientSession().get(url)  
    data = await response.text()  
    return data

    futures = [call_url(url) for url in urls]

    loop = asyncio.get_event_loop()

# async 함수의 return 값이 results에 저장된다.
results = loop.run_until_complete(asyncio.gather(*futures))

for result in results:  
    print(result)

7. 마무리

asyncio 가 기존 스레드 프로그래밍의 문제점을 어떻게 해결했는지 살펴본다

CPU 컨텍스트 스위칭

  • asyncio는 비동기이며 이벤트 루프를 사용하여 I/O를 대기하는 동안 애플리케이션이 컨텍스트 스위칭을 관리한다.

경쟁조건(race conditions)

  • asyncio는 한번에 하나의 코루틴만 실행하며 정의된 지점에서만 스위칭이 일어나기 때문에 경쟁조건에서 안전하다.

Dead-Lock/Live-Locks

  • 경쟁 조건에 대해 걱정할 필요가 없기 때문에 잠금을 사용할 필요가 없다.

기아 상태

  • 모든 코루틴이 하나의 스레드에서 실행되고 추가적인 소켓이나 메모리를 필요로 하지 않는다.
  • 그런데 Asyncio는 기본적으로 스레드 풀인 "executor pool"을 하나 가지고 있기 떄문에, 매우 많은 일들을 하나의 "executor pool" 에서 실행하면 리소스 부족 문제가 발생할 수 있다.

asyncio 를 잘 다룰려면

  • 완전한 비동기를 구현하려면, 모든 코드베이스들이 비동기가 되어야한다.
  • 동기 함수가 너무 많은 시간이 걸려 이벤트 루프를 블로킹할 수도 있기 때문

8. asyncio 에 대한 조금 더 자세한 내용

반응형
  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기