에 대해 이야기 async/await
하고하는 것은 asyncio
같은 일이 아닙니다. 첫 번째는 기본적인 저수준 구조 (코 루틴)이고, 후자는 이러한 구조를 사용하는 라이브러리입니다. 반대로, 궁극적 인 답은 하나도 없습니다.
다음은 방법에 대한 일반적인 설명입니다 async/await
및 asyncio
-like 라이브러리 작동합니다. 즉, 위에 다른 트릭이있을 수 있지만 (...) 직접 구축하지 않는 한 중요하지 않습니다. 그러한 질문을 할 필요가 없을만큼 이미 충분히 알고 있지 않는 한 그 차이는 무시할 만합니다.
1. 너트 쉘의 코 루틴 대 서브 루틴
서브 루틴 (함수, 프로 시저, ...) 과 마찬가지로 코 루틴 (생성자, ...)은 호출 스택과 명령어 포인터의 추상화입니다. 실행중인 코드 조각 스택이 있으며 각각은 특정 명령어에 있습니다.
의 구별 def
대는 async def
명확성을 위해 단지이다. 실제 차이는 return
대 yield
. 이것으로부터 await
또는 yield from
전체 스택에 대한 개별 호출의 차이를 가져옵니다.
1.1. 서브 루틴
서브 루틴은 지역 변수를 보유하는 새로운 스택 레벨과 끝에 도달하기위한 명령어의 단일 순회를 나타냅니다. 다음과 같은 서브 루틴을 고려하십시오.
def subfoo(bar):
qux = 3
return qux * bar
실행하면
bar
및에 대한 스택 공간 할당qux
- 재귀 적으로 첫 번째 명령문을 실행하고 다음 명령문으로 이동
- 한 번에
return
값을 호출 스택으로 푸시합니다.
- 스택 (1.) 및 명령 포인터 (2.)를 지 웁니다.
특히 4.는 서브 루틴이 항상 동일한 상태에서 시작 함을 의미합니다. 함수 자체에 대한 모든 것은 완료시 손실됩니다. 이후에 지침이 있어도 기능을 재개 할 수 없습니다 return
.
root -\
: \- subfoo --\
:/--<---return --/
|
V
1.2. 영구 서브 루틴으로서의 코 루틴
코 루틴은 서브 루틴과 비슷하지만 상태 를 파괴 하지 않고 종료 할 수 있습니다 . 다음과 같은 코 루틴을 고려하십시오.
def cofoo(bar):
qux = yield bar # yield marks a break point
return qux
실행하면
bar
및에 대한 스택 공간 할당qux
- 재귀 적으로 첫 번째 명령문을 실행하고 다음 명령문으로 이동
- 한 번에
yield
해당 값을 호출 스택에 푸시 하지만 스택 및 명령어 포인터를 저장합니다.
- 를 호출하면
yield
스택 및 명령어 포인터를 복원하고 인수를qux
- 한 번에
return
값을 호출 스택으로 푸시합니다.
- 스택 (1.) 및 명령 포인터 (2.)를 지 웁니다.
2.1과 2.2가 추가되었습니다. 코 루틴은 미리 정의 된 지점에서 일시 중단 및 재개 될 수 있습니다. 이것은 다른 서브 루틴을 호출하는 동안 서브 루틴이 일시 중단되는 방법과 유사합니다. 차이점은 활성 코 루틴이 호출 스택에 엄격하게 바인딩되지 않는다는 것입니다. 대신, 일시 중단 된 코 루틴은 별도의 격리 된 스택의 일부입니다.
root -\
: \- cofoo --\
:/--<+--yield --/
| :
V :
즉, 중단 된 코 루틴을 스택간에 자유롭게 저장하거나 이동할 수 있습니다. 코 루틴에 액세스 할 수있는 모든 호출 스택은 코 루틴을 재개하도록 결정할 수 있습니다.
1.3. 호출 스택 탐색
지금까지 코 루틴은 yield
. 서브 루틴은 아래로 갈 수있는 최대 와 호출 스택 return
및 ()
. 완전성을 위해 코 루틴은 호출 스택을 올라가는 메커니즘도 필요합니다. 다음과 같은 코 루틴을 고려하십시오.
def wrap():
yield 'before'
yield from cofoo()
yield 'after'
실행하면 서브 루틴처럼 스택 및 명령어 포인터를 여전히 할당한다는 의미입니다. 일시 중단되면 여전히 서브 루틴을 저장하는 것과 같습니다.
그러나 yield from
않습니다 모두 . 스택 및 명령 포인터를 일시 중단 wrap
하고 실행 cofoo
합니다. 이 참고 wrap
될 때까지 중지 상태로 남아 있습니다 cofoo
완전히 마감. cofoo
일시 중단되거나 무언가가 전송 될 때마다 cofoo
호출 스택에 직접 연결됩니다.
1.4. 끝까지 코 루틴
설정된대로 yield from
다른 중간 범위에 걸쳐 두 범위를 연결할 수 있습니다. 재귀 적으로 적용 하면 스택 맨 위 가 스택 맨 아래 에 연결될 수 있습니다 .
root -\
: \-> coro_a -yield-from-> coro_b --\
:/ <-+------------------------yield ---/
| :
:\ --+-- coro_a.send----------yield ---\
: coro_b <-/
주 root
및 coro_b
서로에 대해 알고하지 않습니다. 이것은 코 루틴을 콜백보다 훨씬 더 깨끗하게 만듭니다. 코 루틴은 여전히 서브 루틴과 같은 1 : 1 관계에 구축됩니다. 코 루틴은 일반 호출 지점까지 전체 기존 실행 스택을 일시 중지하고 다시 시작합니다.
특히 root
재개 할 코 루틴을 임의의 수로 가질 수 있습니다. 그러나 동시에 둘 이상을 재개 할 수는 없습니다. 동일한 루트의 코 루틴은 동시이지만 병렬이 아닙니다!
1.5. 파이썬 async
과await
설명은 지금까지 명시 적으로 사용하고있다 yield
및 yield from
발전기의 어휘 - 기본 기능은 동일합니다. 새로운 Python3.5 구문 async
과 await
주로 명확성을 위해 존재한다.
def foo(): # subroutine?
return None
def foo(): # coroutine?
yield from foofoo() # generator? coroutine?
async def foo(): # coroutine!
await foofoo() # coroutine!
return None
async for
와 async with
당신이 휴식 때문에 문이 필요하다 yield from/await
베어 체인 for
과 with
문을.
2. 간단한 이벤트 루프의 구조
그 자체로 코 루틴은 다른 코 루틴에 대한 제어권을 양보하는 개념이 없습니다 . 코 루틴 스택의 맨 아래에있는 호출자에게만 제어권을 양보 할 수 있습니다. 이 호출자는 다른 코 루틴으로 전환하여 실행할 수 있습니다.
여러 코 루틴의이 루트 노드는 일반적으로 이벤트 루프입니다 . 일시 중단시 코 루틴은 재개하려는 이벤트 를 생성합니다 . 결과적으로 이벤트 루프는 이러한 이벤트가 발생하기를 효율적으로 기다릴 수 있습니다. 이를 통해 다음에 실행할 코 루틴 또는 재개하기 전에 대기하는 방법을 결정할 수 있습니다.
이러한 설계는 루프가 이해하는 사전 정의 된 이벤트 세트가 있음을 의미합니다. await
마지막으로 이벤트가 await
편집 될 때까지 여러 코 루틴이 서로 연결 됩니다. 이 이벤트는 제어 를 통해 이벤트 루프와 직접 통신 할 수 있습니다 yield
.
loop -\
: \-> coroutine --await--> event --\
:/ <-+----------------------- yield --/
| :
| : # loop waits for event to happen
| :
:\ --+-- send(reply) -------- yield --\
: coroutine <--yield-- event <-/
핵심은 코 루틴 서스펜션을 통해 이벤트 루프와 이벤트가 직접 통신 할 수 있다는 것입니다. 중간 코 루틴 스택은 필요하지 않습니다 어떤 루프를 실행중인 대한 지식이나 방법 이벤트 작업을.
2.1.1. 시간의 이벤트
처리하기 가장 간단한 이벤트는 특정 시점에 도달하는 것입니다. 이것은 스레드 코드의 기본 블록이기도합니다. 스레드 sleep
는 조건이 참이 될 때까지 반복적으로 s입니다. 그러나 일반 sleep
블록 실행 자체는 다른 코 루틴이 차단되지 않기를 원합니다. 대신 현재 코 루틴 스택을 재개해야하는시기를 이벤트 루프에 알려야합니다.
2.1.2. 이벤트 정의
이벤트는 단순히 열거 형, 유형 또는 기타 ID를 통해 식별 할 수있는 값입니다. 목표 시간을 저장하는 간단한 클래스로 이것을 정의 할 수 있습니다. 이벤트 정보 를 저장 하는 것 외에도 await
클래스에 직접 허용 할 수 있습니다.
class AsyncSleep:
"""Event to sleep until a point in time"""
def __init__(self, until: float):
self.until = until
# used whenever someone ``await``s an instance of this Event
def __await__(self):
# yield this Event to the loop
yield self
def __repr__(self):
return '%s(until=%.1f)' % (self.__class__.__name__, self.until)
이 클래스 는 이벤트 만 저장 합니다. 실제로 이벤트를 처리하는 방법은 언급하지 않습니다.
유일한 특징은 __await__
- 그것이 무엇 인 await
에 대한 키워드 보인다. 실제로는 반복자이지만 일반 반복 기계에는 사용할 수 없습니다.
2.2.1. 이벤트를 기다리는 중
이제 이벤트가 생겼으니 코 루틴은 어떻게 반응할까요? 우리는 동등한 표현 할 수 있어야한다 sleep
에 의해 await
우리의 이벤트를 보내고. 무슨 일이 일어나고 있는지 더 잘보기 위해 절반의 시간 동안 두 번 기다립니다.
import time
async def asleep(duration: float):
"""await that ``duration`` seconds pass"""
await AsyncSleep(time.time() + duration / 2)
await AsyncSleep(time.time() + duration / 2)
이 코 루틴을 직접 인스턴스화하고 실행할 수 있습니다. 생성기와 유사하게 using coroutine.send
은 yield
결과가 나올 때까지 코 루틴 을 실행합니다 .
coroutine = asleep(100)
while True:
print(coroutine.send(None))
time.sleep(0.1)
이것은 우리에게 두 개의 AsyncSleep
이벤트를 제공 StopIteration
하고 코 루틴이 완료되면 하나를 제공합니다. 유일한 지연은 time.sleep
루프 에서 오는 것 입니다! 각각 AsyncSleep
은 현재 시간의 오프셋 만 저장합니다.
2.2.2. 이벤트 + 수면
이 시점에서 우리는 두 가지 별도의 메커니즘을 사용할 수 있습니다.
AsyncSleep
코 루틴 내부에서 생성 될 수있는 이벤트
time.sleep
코 루틴에 영향을주지 않고 기다릴 수있는
특히,이 두 가지는 직교합니다. 어느 쪽도 다른쪽에 영향을 미치거나 트리거하지 않습니다. 결과적으로 .NET sleep
Framework의 지연에 대처하기 위한 자체 전략을 마련 할 수 있습니다 AsyncSleep
.
2.3. 순진한 이벤트 루프
코 루틴 이 여러 개인 경우 각 코 루틴이 깨어나고 싶은시기를 알 수 있습니다. 그런 다음 첫 번째 항목이 재개되기를 원할 때까지 기다린 다음 이후 항목을 기다릴 수 있습니다. 특히, 각 지점에서 우리는 다음 항목 에만 관심이 있습니다.
이것은 간단한 스케줄링을 만듭니다.
- 원하는 깨우기 시간으로 코 루틴 정렬
- 일어나고 싶은 첫 번째 선택
- 이 시점까지 기다려
- 이 코 루틴을 실행
- 1부터 반복합니다.
사소한 구현에는 고급 개념이 필요하지 않습니다. A list
는 날짜별로 코 루틴을 정렬 할 수 있습니다. 기다리는 것은 규칙적 time.sleep
입니다. 코 루틴을 실행하는 것은 coroutine.send
.
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
# store wake-up-time and coroutines
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting:
# 2. pick the first coroutine that wants to wake up
until, coroutine = waiting.pop(0)
# 3. wait until this point in time
time.sleep(max(0.0, until - time.time()))
# 4. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
물론 이것은 개선의 여지가 충분합니다. 대기 큐에 힙을 사용하거나 이벤트에 디스패치 테이블을 사용할 수 있습니다. 또한 반환 값을 가져올 수 있습니다.StopIteration
코 루틴에 할당 할 수도 있습니다. 그러나 기본 원칙은 동일합니다.
2.4. 협력 대기
AsyncSleep
이벤트와 run
이벤트 루프는 타임 이벤트를 완벽하게 작업을 구현합니다.
async def sleepy(identifier: str = "coroutine", count=5):
for i in range(count):
print(identifier, 'step', i + 1, 'at %.2f' % time.time())
await asleep(0.1)
run(*(sleepy("coroutine %d" % j) for j in range(5)))
이렇게하면 5 개의 코 루틴이 각각 협력 적으로 전환되어 0.1 초 동안 중단됩니다. 이벤트 루프는 동기식이지만 2.5 초가 아닌 0.5 초만에 작업을 실행합니다. 각 코 루틴은 상태를 유지하고 독립적으로 작동합니다.
3. I / O 이벤트 루프
지원하는 이벤트 루프 sleep
는 폴링에 적합합니다 . 그러나 파일 핸들에서 I / O를 기다리는 것은보다 효율적으로 수행 할 수 있습니다. 운영 체제는 I / O를 구현하므로 어떤 핸들이 준비되었는지 알고 있습니다. 이상적으로 이벤트 루프는 명시 적 "I / O 준비"이벤트를 지원해야합니다.
3.1. select
전화
Python에는 이미 OS에 읽기 I / O 핸들을 쿼리하는 인터페이스가 있습니다. 읽기 또는 쓰기 핸들과 함께 호출되면 읽기 또는 쓰기 준비 가 된 핸들을 반환합니다 .
readable, writeable, _ = select.select(rlist, wlist, xlist, timeout)
예를 들어, open
쓸 파일을 작성하고 준비 될 때까지 기다릴 수 있습니다.
write_target = open('/tmp/foo')
readable, writeable, _ = select.select([], [write_target], [])
선택이 반환되면 writeable
열린 파일이 포함됩니다.
3.2. 기본 I / O 이벤트
AsyncSleep
요청과 마찬가지로 I / O에 대한 이벤트를 정의해야합니다. 기본 select
로직에서 이벤트는 읽을 수있는 객체 (예 : open
파일)를 참조해야 합니다. 또한 읽을 데이터의 양을 저장합니다.
class AsyncRead:
def __init__(self, file, amount=1):
self.file = file
self.amount = amount
self._buffer = ''
def __await__(self):
while len(self._buffer) < self.amount:
yield self
# we only get here if ``read`` should not block
self._buffer += self.file.read(1)
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.file, self.amount, len(self._buffer)
)
와 마찬가지로 AsyncSleep
우리 대부분은 단지 기본 시스템 호출에 필요한 데이터를 저장합니다. 이번에 __await__
는 원하는 amount
내용을 읽을 때까지 여러 번 재개 할 수 있습니다. 또한 우리 return
는 재개하는 대신 I / O 결과를 얻습니다.
3.3. 읽기 I / O로 이벤트 루프 확대
이벤트 루프의 기초는 여전히 run
이전 에 정의 된 것입니다. 먼저 읽기 요청을 추적해야합니다. 이것은 더 이상 정렬 된 일정이 아니며 읽기 요청 만 코 루틴에 매핑합니다.
# new
waiting_read = {} # type: Dict[file, coroutine]
이후 select.select
시간 초과 매개 변수를, 우리는 대신에 사용할 수 있습니다 time.sleep
.
# old
time.sleep(max(0.0, until - time.time()))
# new
readable, _, _ = select.select(list(reads), [], [])
이것은 우리에게 읽을 수있는 모든 파일을 제공합니다. 만약 있다면 해당 코 루틴을 실행합니다. 없는 경우 현재 코 루틴이 실행될 때까지 충분히 기다렸습니다.
# new - reschedule waiting coroutine, run readable coroutine
if readable:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read[readable[0]]
마지막으로 실제로 읽기 요청을 들어야합니다.
# new
if isinstance(command, AsyncSleep):
...
elif isinstance(command, AsyncRead):
...
3.4. 합치기
위의 내용은 약간 단순화되었습니다. 우리가 항상 읽을 수 있다면 잠자는 코 루틴을 굶지 않도록 약간의 전환을해야합니다. 우리는 읽을 것이 없거나 기다릴 것이없는 것을 처리해야합니다. 그러나 최종 결과는 여전히 30 LOC에 맞습니다.
def run(*coroutines):
"""Cooperatively run all ``coroutines`` until completion"""
waiting_read = {} # type: Dict[file, coroutine]
waiting = [(0, coroutine) for coroutine in coroutines]
while waiting or waiting_read:
# 2. wait until the next coroutine may run or read ...
try:
until, coroutine = waiting.pop(0)
except IndexError:
until, coroutine = float('inf'), None
readable, _, _ = select.select(list(waiting_read), [], [])
else:
readable, _, _ = select.select(list(waiting_read), [], [], max(0.0, until - time.time()))
# ... and select the appropriate one
if readable and time.time() < until:
if until and coroutine:
waiting.append((until, coroutine))
waiting.sort()
coroutine = waiting_read.pop(readable[0])
# 3. run this coroutine
try:
command = coroutine.send(None)
except StopIteration:
continue
# 1. sort coroutines by their desired suspension ...
if isinstance(command, AsyncSleep):
waiting.append((command.until, coroutine))
waiting.sort(key=lambda item: item[0])
# ... or register reads
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
3.5. 협력 I / O
AsyncSleep
, AsyncRead
및 run
구현은 이제 수면 및 / 또는 읽기에 완벽하게 작동합니다. 에서와 마찬가지로 sleepy
읽기를 테스트하는 도우미를 정의 할 수 있습니다.
async def ready(path, amount=1024*32):
print('read', path, 'at', '%d' % time.time())
with open(path, 'rb') as file:
result = return await AsyncRead(file, amount)
print('done', path, 'at', '%d' % time.time())
print('got', len(result), 'B')
run(sleepy('background', 5), ready('/dev/urandom'))
이를 실행하면 I / O가 대기 작업과 인터리브되는 것을 볼 수 있습니다.
id background round 1
read /dev/urandom at 1530721148
id background round 2
id background round 3
id background round 4
id background round 5
done /dev/urandom at 1530721148
got 1024 B
4. 비 블로킹 I / O
파일에 대한 I / O가 개념을 이해하는 동안에는 다음과 같은 라이브러리에는 적합하지 않습니다 asyncio
. select
호출은 항상 파일 에 대해 반환 되며 둘 다 open
및 둘 다 무기한 차단read
될 수 있습니다 . 이것은 이벤트 루프의 모든 코 루틴을 차단합니다. 스레드 및 동기화와 같은 라이브러리 는 파일의 비 차단 I / O 및 이벤트를 위조합니다.aiofiles
그러나 소켓은 비 차단 I / O를 허용하며 고유 한 대기 시간으로 인해 훨씬 더 중요합니다. 이벤트 루프에서 사용될 때, 데이터를 기다리고 재 시도하는 것은 아무것도 차단하지 않고 래핑 될 수 있습니다.
4.1. 비 블로킹 I / O 이벤트
와 유사하게 AsyncRead
소켓에 대한 일시 중지 및 읽기 이벤트를 정의 할 수 있습니다. 파일을 가져 오는 대신 소켓을 가져옵니다.이 소켓은 차단되지 않아야합니다. 또한, 우리의 __await__
사용 socket.recv
대신에 file.read
.
class AsyncRecv:
def __init__(self, connection, amount=1, read_buffer=1024):
assert not connection.getblocking(), 'connection must be non-blocking for async recv'
self.connection = connection
self.amount = amount
self.read_buffer = read_buffer
self._buffer = b''
def __await__(self):
while len(self._buffer) < self.amount:
try:
self._buffer += self.connection.recv(self.read_buffer)
except BlockingIOError:
yield self
return self._buffer
def __repr__(self):
return '%s(file=%s, amount=%d, progress=%d)' % (
self.__class__.__name__, self.connection, self.amount, len(self._buffer)
)
반면에 AsyncRead
, __await__
수행은 참으로 I / O를 비 차단. 데이터를 사용할 수 있으면 항상 읽습니다. 사용 가능한 데이터가 없으면 항상 일시 중지됩니다. 즉, 유용한 작업을 수행하는 동안에 만 이벤트 루프가 차단됩니다.
4.2. 이벤트 루프 차단 해제
이벤트 루프에 관한 한 큰 변화는 없습니다. 수신 대기 할 이벤트는으로 준비된 것으로 표시된 파일 설명자인 파일과 동일합니다 select
.
# old
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
# new
elif isinstance(command, AsyncRead):
waiting_read[command.file] = coroutine
elif isinstance(command, AsyncRecv):
waiting_read[command.connection] = coroutine
이 시점에서, 그 명백해야 AsyncRead
하고 AsyncRecv
이벤트 같은 종류입니다. 교환 가능한 I / O 구성 요소 가있는 하나의 이벤트로 쉽게 리팩터링 할 수 있습니다 . 실제로 이벤트 루프, 코 루틴 및 이벤트 는 스케줄러, 임의의 중간 코드 및 실제 I / O를 명확하게 분리 합니다.
4.3. 논 블로킹 I / O의 추악한면
원칙적으로이 시점에서해야 할 일은 for 의 논리를 복제 read
하는 것 recv
입니다 AsyncRecv
. 그러나 이것은 훨씬 더 추악합니다. 함수가 커널 내부에서 차단 될 때 조기 리턴을 처리해야하지만 제어권을 양보해야합니다. 예를 들어, 연결을 여는 것보다 파일을 여는 것이 훨씬 더 깁니다.
# file
file = open(path, 'rb')
# non-blocking socket
connection = socket.socket()
connection.setblocking(False)
# open without blocking - retry on failure
try:
connection.connect((url, port))
except BlockingIOError:
pass
간단히 말해서, 남은 것은 수십 줄의 예외 처리입니다. 이벤트와 이벤트 루프는 이미이 시점에서 작동합니다.
id background round 1
read localhost:25000 at 1530783569
read /dev/urandom at 1530783569
done localhost:25000 at 1530783569 got 32768 B
id background round 2
id background round 3
id background round 4
done /dev/urandom at 1530783569 got 4096 B
id background round 5
추가
github의 예제 코드
BaseEventLoop
이 어떻게 구현 되는지 살펴보세요 : github.com/python/cpython/blob/…