- Python의 asyncio는 I/O-bound 프로그램을 다루는 도구이며, 글은 이를 제너레이터 위에 단순 구현으로 다시 만들어 작동 원리를 보여줌
- 핵심은 여러 작업을 가진 이벤트 루프가 작업을 실행하다가
yield에서 제어권을 돌려받고, 다음 작업으로 넘어가는 구조임 sleep은 시간이 지날 때까지yield하는 서브 제너레이터로 만들 수 있으며,yield from은 하위 제너레이터가 끝난 뒤 원래 작업을 이어가게 함async/await로 바꾸면Task.__await__()가 완료 전까지 이벤트 루프로 제어권을 넘기고,create_task와run이 실제 asyncio API와 비슷한 형태를 만듦- 커스텀
jacobio호출을asyncio로 바꾸면 실제 패키지를 쓰는 코드가 되지만, 실제 asyncio는 예제보다 훨씬 복잡하고 내부 흐름도 일부 다름
제너레이터로 보는 asyncio의 기본 구조
- asyncio는 Python에서 I/O-bound 프로그램을 처리하는 데 쓰이며, 글은 이를 제너레이터 기반으로 단순화해 재구성함
- Python 3+의
range처럼 제너레이터는 전체 시퀀스를 메모리에 저장하지 않고 필요한 값을 하나씩 생성함range(100_000_000)을 리스트처럼 만들면 1억 개 요소를 저장해야 하므로 메모리 비효율이 큼- 제너레이터는 값이 필요할 때마다 생성하므로 전체 시퀀스를 보관하지 않음
- 제너레이터 함수는 일반 함수처럼 정의하지만
yield를 사용함- 함수 호출 시 본문을 즉시 실행하지 않고 제너레이터 객체를 반환함
next(iterator)를 호출하면 다음yield까지 실행됨- 더 이상
yield가 없으면StopIteration예외가 발생함
yield from은 제너레이터가 서브 제너레이터나 iterable 객체를 호출하게 해 제너레이터 체인을 만들 수 있음- 여기서 중요한 특성은 함수 실행을 멈췄다가 상태를 유지한 채 다시 시작할 수 있다는 점임
이벤트 루프를 리스트로 단순화하기
- 이벤트 루프는 현재 작업들을 실행하고 관리하는 asyncio의 핵심임
- 실제 asyncio 이벤트 루프는 C로 작성되어 있지만, 단순 모델에서는 현재 작업을 담는 리스트로 볼 수 있음
- 초기 예제에서 작업은 제너레이터 객체로 취급됨
- 이벤트 루프 관리자가 작업 리스트를 순회함
- 각 작업에
next(task)를 호출해 실행함 - 작업은 I/O-bound 작업처럼 기다려야 하는 시점에
yield로 멈추고 이벤트 루프에 제어권을 돌려줌
task1()과task2()가 각각 출력 후yield하는 예제에서는 출력이 번갈아 나타남- 두 함수가
while True루프를 가지므로 실행은 계속 이어짐 - 출력은
Task 1,Task 2,Task 1,Task 2처럼 반복됨
- 두 함수가
yield from으로 sleep 만들기
sleep(seconds)는 시작 시간을 기록한 뒤 지정한 시간이 지나기 전까지 계속yield하는 제너레이터로 구현됨- 작업 함수는
yield from sleep(1)또는yield from sleep(5)처럼 sleep 서브 제너레이터에 실행을 위임함sleep이 계속yield하는 동안 작업은 멈춘 상태가 됨- 시간이 충분히 지나면
sleep의while루프가 끝남 - 더 이상
yield가 없으므로StopIteration이 발생하고,yield from은 작업 함수의 다음 줄로 이어짐
- 예제에서는
task1이 1초마다 출력하고task2가 5초마다 출력함- 출력은
Task 1,Task 2이후Task 1이 여러 번 나오고 다시Task 2가 나오는 형태가 됨
- 출력은
yield에서 await로 바꾸기
await를 사용하려면 대상 객체가__await__메서드를 가지거나 coroutine이어야 함- asyncio에서는 보통
asyncio.create_task같은 함수로Task객체를 다룸Task객체는 asyncio의Future객체를 상속함Future객체에는__await__메서드가 있음
async키워드가 붙은 함수를 호출하면 coroutine 객체가 만들어짐- coroutine은 제너레이터 함수처럼 실행을 멈췄다가 다시 이어갈 수 있음
await는 추가 검증 규칙이 있는yield from과 비슷하게 볼 수 있음await object는 객체 인스턴스의__await__에서yield하거나, 다른 coroutine을 기다리는 형태가 됨
- asyncio의
Future소스 코드에서도Future또는Task가 완료되지 않았을 때__await__가 기본적으로yield를 호출하는 형태를 볼 수 있음
단순 Task, create_task, run 구현
- 커스텀 구현은 리스트 대신
Queue를 이벤트 루프로 사용함- 작업을 루프에 추가하고 삭제하는 동작을 상수 시간에 처리하려는 구조임
Task클래스는 제너레이터 객체와 완료 여부를 저장함self.iter에 제너레이터 객체를 저장함self.finished를False로 시작함- 제너레이터가
StopIteration을 발생시키면 완료된 것으로 처리함 done()은 완료 상태를 반환함
Task.__await__()는 작업이 끝나지 않은 동안 계속yield self를 호출함- 이 동작이 이벤트 루프에 제어권을 돌려줌
create_task(generator)는 제너레이터를Task로 감싸고 이벤트 루프 큐에 넣은 뒤 반환함- 작업을 이벤트 루프에 스케줄링하는 역할임
run(main)은 실제asyncio.run()과 비슷한 모양으로 이벤트 루프를 시작함- 처음 받은
main을Task로 감싸 큐에 넣음 - 큐가 비어 있지 않은 동안 다음 작업을 꺼냄
task.iter.send(None)으로 작업을 진행함StopIteration이 발생하면task.finished = True로 설정함- 예외가 없으면 작업을 다시 이벤트 루프 큐에 넣음
- 처음 받은
next(task.iter)대신task.iter.send(None)을 쓰는 것은async/await키워드와 함께 작업할 때의 특성으로, 여기서는 같은 역할을 함
async 호환 sleep과 jacobio 예제
- 기존
sleep은 제너레이터 함수였지만,await는 제너레이터 함수와 직접 조합할 수 없음await대상은__await__를 가진 객체이거나 coroutine 함수여야 함
- 실제 대기 로직은
_sleep(seconds)제너레이터로 옮김_sleep은 일정 시간이 지날 때까지yield함
async def sleep(seconds)는_sleep(seconds)로 작업을 만들고 그 작업을await함await task는Task.__await__()를 호출함- 작업이 끝나지 않았으면
yield로 이벤트 루프에 제어권을 넘김
- 완성된 커스텀 파일
jacobio.py는 다음 요소를 포함함- 이벤트 루프 큐
_sleepasync sleepTaskcreate_taskrun
- 사용 예제에서는 기존
yield from을await로 바꾸고,await를 쓰는 함수에async를 붙임task1은 두 번 출력하고 매번jacobio.sleep(1)을 기다림task2는 세 번 출력하고 매번jacobio.sleep(0)을 기다림main은 두 작업을 만들고 둘 다await한 뒤done을 출력함
- 예제 출력은
Task 1,Task 2,Task 2,Task 2,Task 1,done순서임
실제 asyncio로 바꾸기
- 커스텀
jacobio예제에서jacobio를 모두asyncio로 바꾸면 실제 asyncio 패키지를 사용하는 코드가 됨 - 대응되는 함수는 다음과 같음
jacobio.sleep()→asyncio.sleep()jacobio.create_task()→asyncio.create_task()jacobio.run()→asyncio.run()
- 실제 asyncio는 내부에서 훨씬 더 많은 일을 함
- 이 이벤트 루프 관리자는 가능한 단순하게 만든 구현이라 asyncio의 기본 아이디어는 보여주지만, 실제 패키지의 규모와 복잡성 때문에 실제 소스 코드 흐름과는 조금 다름
- 실제 asyncio를 쓰면 두 작업을 각각 만들어 둘 다
await하는 대신asyncio.gather()같은 함수로 여러 작업을 처리할 수 있음 - 관련 글로 handling asyncio tasks like a pro가 연결됨