python은 single thread로 동작하는 인터프리터 언어이기 때문에 동기(synchronous=순차적으로)방식으로 코드를 실행하고 완료가 된 후에 다음 작업을 실행한다.
하지만 대기시간이 많은 I/O작업 같은 경우 sync 방식으로 구현되어 있으면 비효율적이기 때문에 function, method 등이 완료되는 것을 기다리지 않고 다음 작업을 진행하는 비동기(asynchronous) 방식을 사용한다.
asyncio는 비동기 I/O를 지원하는 python의 내장 라이브러리로 coroutine과 task를 사용하여 비동기 프로그래밍을 지원한다.
asyncio는 event loop을 기반으로 비동기 함수(async def
)와 비동기 작업을 관리한다.
coroutine은 비동기 함수의 일종으로, 일시 중단되었다가 나중에 다시 시작될 수 있는 함수로 async
를 붙여서 정의한다.
await
를 사용하여 다른 coroutine을 호출하거나 비동기 작업을 기다릴 수 있다.
아래 예시의 경우 say_after(1, 'hello')
라는 coroutine이 실행 된 후에 say_after(2, 'world')
라는 coroutine이 실행되기 때문에 총 3초가 걸린다.
import asyncio
import time
async def say_after(delay, what):
await asyncio.sleep(delay)
print(what)
async def main():
print(f"started at {time.strftime('%X')}")
await say_after(1, 'hello')
await say_after(2, 'world')
print(f"finished at {time.strftime('%X')}")
asyncio.run(main())
출력
started at 17:13:52
hello
world
finished at 17:13:55
task는 coroutine을 실행하기 위해 event loop에 의해 관리되는 객체로 생성되자마자 비동기적으로 실행된다.
task는 coroutine을 wrapping 하여 실행 상태를 관리하고, 실행 결과를 추적할 수 있다.
아래 예시의 경우 task1
을 생성하고 task2
를 생성한 후에 await
을 통해 비동기적으로 실행된 task의 결과를 기다리기 때문에 총 2초가 걸린다.
async def main():
task1 = asyncio.create_task(
say_after(1, 'hello'))
task2 = asyncio.create_task(
say_after(2, 'world'))
print(f"started at {time.strftime('%X')}")
# Wait until both tasks are completed (should take
# around 2 seconds.)
await task1
await task2
print(f"finished at {time.strftime('%X')}")
예상 출력
started at 17:14:32
hello
world
finished at 17:14:34
coroutine | task | |
---|---|---|
정의 | async def 로 정의된 함수 | asyncio.create_task 로 생성된 coroutine의 wrapper |
실행방식 | await 를 통해서 호출됨으로써 실행 | 생성되자마자 event loop에 의해 스케줄링되어 병렬로 실행 |
목적 | 비동기 작업을 정의하기 위한 기본 단위 | coroutine을 실행 관리하여 비동기 작업을 동시에 수행할 수 있게 함 |
상태관리 | 상태 관리하지 않음 | 실행 상태, 결과 및 예외 추적 |
uvloop
은 node.js에서 사용되는 고성능 비동기 I/O 라이브러리인 libuv
를 기반으로 구현된 파이썬용 이벤트 루프로 libuv
를 사용하여 파이썬의 event loop를 대체함으로써 asyncio
보다 훨씬 빠른 성능을 제공한다.
많은 벤치마크 테스트에서 uvloop
은 asyncio
이벤트 루프에 비해 2배에서 4배 정도 빠른 성능을 보였다.
uvloop
을 사용하기 위해서는 event loop정책을 uvloop
로 설정하면 된다.
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
async def main():
print("Hello, World!")
asyncio.run(main())
하지만 파이썬 표준 라이브러리인 asyncio
와 다르게 uvloop
는 서드파티 라이브러리라서 python 버전이나 특정 라이브러리와의 dependency 문제가 발생할 수 있으며 도입전에 많은 테스트를 거쳐야 한다.
python web api 구현에 자주 사용되는 fastapi의 경우 v0.111.0부터 uvloop을 dependency library로 설치해서 사용하고 있다.
기본적으로 asyncio
나 uvloop
의 경우 중첩된 event loop의 실행을 허용하지 않는데, nest_asyncio
는 기존의 event loop가 실행중인 상황에서도 새로운 event loop를 중첩하여 실행할 수 있도록 한다.
주로 jupyter notebook나 fastapi와 같은 비동기 프레임워크에서 사용중인 event loop와 개발자가 구현한 event loop 간의 충돌을 피하기 위해 사용되며 성능 향상을 목적으로 하지는 않는다.
nest_asyncio
를 사용하기 위해서는 nest_asyncio.apply()
를 호출하기만 하면 된다.
import nest_asyncio
import asyncio
nest_asyncio.apply()
async def main():
print("Hello, World!")
asyncio.run(main())