Posts Python에서 asyncio의 동작
Post
Cancel

Python에서 asyncio의 동작

python - How does asyncio actually work? - Stack Overflow

해당글을 번역한 글입니다.

파이썬에서 asnyccoroutine을 기반으로 동작합니다. coroutine의 동작을 이해하기 전 generator의 개념을 알고가면 이해하기 쉽기 때문에 먼저 generator 개념 부터 설명하겠습니다.

Generator

generatoryield 문법을 사용하여 구현한 함수 형태입니다. 독특한 점은 yield를 통해 함수에서 다시 호출한 곳으로 제어권을 넘긴다는 점입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def test():
    yield 1
    yield 2
    yield 3
    yield 4
    
a = test()
print(next(a))
#1
print(next(a))
#2
print(next(a))
#3
print(next(a))
#4
print(next(a))

generator에서 next()를 호출하면 인터프리터가 test의 프레임을 로드하고 산출된 값을 반환합니다. next()를 다시 호출하면 test 프레임이 인터프리터 스택에 다시 로드되고 계속해서 다른 값을 산출합니다.

4번의 next()를 호출하면 generatorStopIteration 예외을 발생시켜 generator가 끝났음을 알립니다.

Generator 통신하기

generatorsend()throw()의 두 가지 메서드를 사용하여 이들과 통신 할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def test():
    a = yield 1
    print(a)
    yield 2

g = test()

print(next(a))
# 1
print(next(a))
# None
# 2
print(next(a))
# StopIteration

g = test()
print(next(a))
# 1
g.send(123)
# 123
# 2

send로 값을 보내면 yield 문법으로 값을 돌려받는 받습니다.

g.throw()generator 내부에서 yield 가 있는 위치에서 Exception을 던질 수 있도록 허용합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def test():
    a = yield 1
    print(a)
    yield 2

g = test()

print(next(a))
# 1
g.throw(Exception())
# Traceback (most recent call last):
#   File "<stdin>", line 2, in <module>
#   File "<stdin>", line 2, in test
# Exception

Generator로부터 반환값 받기

generator로부터 반환값을 받으려면 결과값은 StopIteration 예외 내부에서 값을 할당 받아야 한다. 나중에 예외에서 값을 복구하여 필요에 따라 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def test():
    yield 1
    return "hello"

g = test()

print(next(a))
# 1

try:
    print(next(a))
except StopIteration as e:
    e.value
    # hello

새로운 문법 yield from

Python3.4에서 yield from이라는 새로운 문법이 소개 되었다. yield from은 가장 안쪽의 generator에게 next(), send(), throw()를 넘겨주는 것을 할 수 있다. 만약 내부의 generatorreturn했다면 yield from으로 인해 외부의 generator도 return한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def inner():
    a = yield 1

    print(a)

    return "done"


def outer():
    yield 2
    val = yield from inner()
    print(val)
    yield 4


g = outer()

print(next(g))
# 2
print(next(g))
# 1
g.send("hello")
# hello
# done
print(next(g))
#Traceback (most recent call last):
#  File "/Users/gim-uichan/study/python_async/test.py", line 26, in <module>
#    print(next(g))
#StopIteration: hi

Coroutine

yield from문을 사용하여 generator에서 터널처럼 내부 generator를 사용할 수 있게 되었습니다. 데이터를 가장 안쪽에서 바깥 쪽 generator로 앞뒤로 전달합니다. 이것은 generator에게 새로운 의미를 낳았습니다. 우리가 알고 있는 couroutine과 가장 비슷한 개념이 생겼습니다.

coroutine은 실행 도중 일시 정지와 재 시작할 수 있는 함수입니다. 파이썬에서는 async def를 사용해서 만들 수 있습니다. generator와 유사하게 yield from과 비슷한 await을 사용합니다. Python 3.5에서 async, await이 소개되기 전 generator와 같은 방식으로 coroutine을 만들었습니다.(await 대신에 yield from) coroutineawait coro가 호출될 때마다 실행될 수 있도록 __await __()를 구현합니다.

Coroutine chain

18.5.3. Tasks and coroutines — Python 3.5.10 documentation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import asyncio

async def compute(x, y):
    print("Compute %s + %s ..." % (x, y))
    await asyncio.sleep(1.0)
    return x + y

async def print_sum(x, y):
    result = await compute(x, y)
    print("%s + %s = %s" % (x, y, result))

loop = asyncio.get_event_loop()
loop.run_until_complete(print_sum(1, 2))
loop.close()

compute()print_sum()에 연결되어있습니다. print_sum()compute()가 끝나 결과를 반환할 때까지 기다립니다.

tulip_coro.png

TaskAbstractEventLoop.run_until_complete() 메소드에서 task가 아닌 coroutine 객체를 통해 성성됩니다.

asyncio coroutine에서 중요한 2가지 object tasks, futures가 있습니다.

Futures

futures__await__() 메소드를 구현한 객체입니다. 그들의 임무는 특정 상태와 결과를 유지하는 것입니다. 상태는 다음 중 하나 일 수 있습니다.

  • PENDING: future가 결과나 예외를 갖고 있지 않음.
  • CANCELLED: futurefut.cancel()에 의해 취소됨.
  • FINISHED: 결과 값이 fut.set_result()로 설정되거나 예외가 fut.set_exception()로 설정되어 future가 종료됨.

future의 결과는 반환될 Python 객체거나 발생할 수 있는 예외입니다.

future 객체의 또 다른 중요한 기능은 add_done_callback()이라는 메서드를 포함한다는 것입니다. 이 메서드를 사용하면 예외가 발생 했든 완료 되었든 작업이 완료되는 즉시 callback 함수를 호출합니다.

Tasks

task 객체는 coroutine을 감싸고 가장 안쪽 및 가장 바깥 쪽 coroutine과 통신하는 특별한 futures입니다. coroutinefuture를 기다릴 때마다 future는 작업으로 완전히 다시 전달되고 (yield from 과 같이) task은 이를 수신합니다.

taskfutureadd_done_callback()을 호출하여 자신을 future에 바인딩합니다. future가 예외를 던지거나 결과값을 반환하여 완료되면 taskfuture에 바인딩했던 콜백이 호출되고 다시 실행하게됩니다.

Asyncio

asyncio 내부에는 task 이벤트 루프가 있습니다. 이벤트 루프의 역할은 task이 준비 될 때마다 task을 호출하고 모든 task들이 단일 머신에서 돌아가도록 제어합니다.

이벤트 루프의 IO 부분은 select라는 하나의 중요한 기능을 기반으로합니다. Select는 운영 체제 아래에서 구현된 블록킹 함수로, 소켓에서 들어 오거나 나가는 데이터를 대기 할 수 있습니다. 데이터가 수신되면 깨어나서 데이터를 수신한 소켓을 반환하거나 쓰기 준비가 된 소켓을 반환합니다.

asyncio를 통해 소켓을 통해 데이터를 받거나 보내려고 할 때 맨 처음 일어나는 일은 소켓에 즉시 읽거나 보낼 수 있는 데이터가 있는지 먼저 확인하는 것입니다. .send() 버퍼가 가득 차거나 .recv() 버퍼가 비어있는 경우 소켓은 select 함수에 등록됩니다 (단순히 목록 중 하나에 추가, recv의 경우 rlist, 송신의 경우 wlist). 함수는 해당 소켓에 연결된 새로 생성 된 future 객체를 기다립니다.

사용 가능한 모든 taskfuture를 기다리고있을 때 이벤트 루프는 select를 호출하고 대기합니다. 소켓 중 하나에 들어오는 데이터가 있거나 전송 버퍼가 비워지면 asyncio는 해당 소켓에 연결된 future 객체를 확인하고 완료로 설정합니다.

이제 모든 마법이 일어납니다. future는 완료로 설정되고 add_done_callback()으로 이전에 추가 된 작업이 다시 살아 나고 가장 안쪽의 coroutine을 재개하는 coroutine에서 .send()를 호출합니다 (await 체인으로 인해). 근처 버퍼에서 새로 수신된 데이터가 유출되었습니다.

recv()의 경우 다시 메서드 체인 :

  1. select.select가 대기합니다.
  2. 데이터가있는 준비 소켓이 리턴됩니다.
  3. 소켓의 데이터는 버퍼로 이동됩니다.
  4. future.set_result()가 호출됩니다.
  5. add_done_callback()으로 자신을 추가한 task가 깨어납니다.
  6. taskcoroutine에서 .send()를 호출하여 가장 안쪽의 coroutine을 깨웁니다.
  7. 데이터는 버퍼에서 읽고 겸손한 사용자에게 반환됩니다.

요약하면 asyncio는 기능을 일시 중지하고 다시 시작할 수 있는 coroutine 기능을 사용합니다. 가장 안쪽의 coroutine에서 바깥쪽으로 데이터를 앞뒤로 전달할 수있는 기능의 yield from를 사용합니다. IO가 완료되기를 기다리는 동안 (OS select 기능을 사용하여) 기능 실행을 중지하기 위해이 모든 것을 사용합니다.

그리고 무엇보다도 한 기능이 일시 중지 된 동안 다른 기능이 실행되고 asyncio와 교차로 실행될 수 있습니다.

Event loop

asyncio의 핵심으로 task(coroutine)를 관리하는 전략이 핵심이다.

이벤트 루프는 프로그램 에서 이벤트 또는 메시지 를 기다렸다가 전달합니다. 내부 또는 외부 “이벤트 공급자”(일반적으로 이벤트가 도착할 때까지 요청을 차단 함) 에 요청한 다음 관련 이벤트 처리기를 호출 합니다 ( “이벤트 디스패치”).

핵심 기능

  • 스케줄링
  • 네트워크를 통한 데이터 전송
  • DNS 쿼리
  • OS signal 핸들링
  • 서버와 커넥션을 만드는 편리한 추상화
  • 비동기적 서브프로세스(코루틴을 뜻하는것일까?)
  • 호출 등록, 실행 및 취소

macOS Python3.7 버전에서는 I/O Multiplexing을 select를 사용하는 것으로 보입니다.

select 함수를 사용하는 것이 아니라 python selector 모듈에 환경에 맞 멀티 I/O 플렉싱 모델을 기반으로 사용합니다.

1
2
3
import asyncio
print(asyncio.get_event_loop())
# <_UnixSelectorEventLoop running=False closed=False debug=False>

함께보면 좋을 자료

파이썬 Asyncio 를 이해하기 위한 여정 Python의 asyncio를 직접 만들어보자 (3) - 이상한모임 I/O Multiplexing(select, poll, epoll, kqueue) | Mimul Tech log Understanding event loop with Python | by Ilya Pekelny | Medium libuv-vs-libev.md · GitHub

참고자료

Python Generators/Coroutines/Async IO with examples | by Alex Anto Navis L | Analytics Vidhya | Medium 18.5.3. Tasks and coroutines — Python 3.4.10 documentation

This post is licensed under CC BY 4.0 by the author.