파이썬 asyncio 사용법
저번에 4가지 비동기 라이브러리를 비교해보고 asyncio 라이브러리가 제일 빠르다는 것을 알게 되었다. 따라서 이번 글에서는 asyncio 라이브러리 사용법을 공부하고 정리해보았다.
코루틴 (coroutine)
함수는 누군가 call하면 실행되고 결과값을 return하면 함수가 종료되는 방식이다. 반면 코루틴은 여기에 suspend/resume이 가능하다. 즉, 결과값을 바로 return하지 않고 suspend(또는 yield) 할 수 있으며 중단된 지점부터 resume할 수 있다.
제너레이터
제너레이터는 코루틴의 한 형태이다. 파이썬에서 제너레이터의 방식을 활용하는 대표적인 함수로 range()
가 있다. range() 함수는 숫자를 미리 생성해서 리스트로 리턴하지 않고, 제너레이터를 리턴하듯 range 클래스를 리턴한다. range 클래스를 for문에서 사용할 경우 내부적으로 매번 다음 숫자를 생성한다. 따라서 range() 안의 숫자가 아무리 커져도 메모리 점유율이 일정하다는 장점이 있다.
네이티브 코루틴
제너레이터 외에도 async/await 코루틴이 있다. 파이썬에서는 제너레이터 기반의 코루틴과 구분하기 위해 async/await 코루틴을 네이티브 코루틴이라고 부른다.
async/await 문법으로 선언된 코루틴은 asyncio를 사용하여 비동기 프로그래밍을 작성하는 기본 방법이다. async 함수는 await에서 suspend되고, await 대상의 값이 준비되면 resume되어 실행을 이어간다.
- 코루틴 함수 :
async def
함수 - 코루틴 객체 : 코루틴 함수를 호출하여 반환된 객체
코루틴 실행
방금 생성한 main 코루틴 객체는 asyncio.run()
함수로 실행시킬 수 있다. asyncio.run()은 인자로 전달된 코루틴을 실행하고 결과를 반환하는 함수이다. 이 함수는 항상 새 이벤트 루프를 만들고 끝에 이벤트 루프를 닫는다. 따라서 asyncio 프로그램의 메인 진입 지점으로 사용해야하고, 이상적으로는 한 번만 호출해야 한다. 또한 다른 이벤트 루프가 같은 스레드에서 실행 중일 때 해당 함수를 호출할 수 없다.
await / awaitable
네이티브 코루틴은 await 뒤에 지정한 객체의 실행이 끝날 때까지 기다린 뒤 결과를 반환하면 다시 resume되어 실행을 이어나간다. 이때, await 뒤에 지정할 수 있는 객체를 ‘어웨이터블 객체’라 하고, 어웨이터블 객체에는 코루틴, 태스크, 퓨처가 있다.
코루틴
import asyncio
async def nested():
return 42
async def main():
# await하지 않으면 nested코루틴은 생성되었지만 실행되지 않음
nested()
# await로 nested코루틴 실행
print(await nested()) # 42가 출력됨
태스크
태스크는 코루틴이 동시에 실행되도록 예약할 때 사용된다. 코루틴을 asyncio.create_task()
함수로 감싸면 해당 코루틴은 실행되도록 예약되고, Task 객체를 반환한다.
import asyncio
async def nested():
return 42
async def main():
# nested코루틴이 main코루틴과 동시에 실행되도록 예약
task = asyncio.create_task(nested())
await task
코루틴 VS 태스크
코루틴과 실행 결과
import asyncio, timeasync 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())
위 코드를 실행한 결과 약 3초가 소요되었다. 비동기 프로그래밍을 사용하였는데, 왜 두 개의 say_after
작업을 순차적으로 처리했을 때와 실행 시간이 동일할까?
async def로 선언된 함수를 호출하면, 코루틴 객체를 리턴만 하기 때문이다. 즉, say_after()
호출로 코루틴 객체가 생성되고, await에 의해 해당 코루틴 객체가 실행된다. 결과적으로 say_after(1, ‘hello')
와 say_after(2, ‘world')
를 동기적으로 처리한 것이다.
태스크와 실행 결과
import asyncio, timeasync def say_after(delay, what):
await asyncio.sleep(delay)
print(what)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')}") await task1
await task2 print(f"finished at {time.strftime('%X')}")asyncio.run(main())
우리가 원하는대로 두 개의 say_after
코루틴 객체를 동시에 실행하기 위해서는 위의 코드처럼 테스트 객체로 만들어줘야 한다.
create_task
는 반환된 코루틴 객체를 비동기 작업 객체인 테스크로 만들고 실행한다. 따라서 create_task
로 호출했을 때 say_after
함수 내 작성한 코드를 비동기로 실행하게 된다. await에서는 이미 실행한 코드가 종료될 때까지 대기한다.
결론
결론적으로 비동기 방식으로 여러 작업을 하기 위해서는 코루틴을 생성하고 태스크로 만들어주거나 asyncio.gather()
함수를 사용해야 한다. 즉, async def
로 생성한 코루틴을 그대로 await를 하면 동기 실행되고, 태스크로 만들거나 asyncio.gather()
로 호출하면 비동기로 실행되는 것이다.