간단한 비동기 로직을 구현해보고 python 에서 네이티브 코루틴이 어떻게 작동하는지 이해해보겠습니다. (async def 키워드는 python3.5v 이상부터 지원됩니다.)
import asyncio, time, datetime
def 쿨쿨():
time.sleep(5)
print('깼당!')
def 재우기():
for i in range(5):
쿨쿨()
std = datetime.datetime.now()
print(f'Started at {std}')
재우기()
edd = datetime.datetime.now()
print(f'Ended at {edd}')
print(f'Total time = {edd - std}')
쿨쿨 이라는 함수는 5초 동안의 sleep 을 호출하고 재우기 에서는 쿨쿨을 5번 호출합니다.
Started at 2022-07-02 19:40:04.979260
깼당!
깼당!
깼당!
깼당!
깼당!
Ended at 2022-07-02 19:40:29.992931
Total time = 0:00:25.013671
5초 sleep 하는 함수를 5번 호출한다면 모두 완료되는 시점까지 동기식일 경우 25+a 초가 필요합니다. 이를 비동기로 구현해보겠습니다.
import asyncio, time, datetime
async def 쿨쿨():
await loop.run_in_executor(None, time.sleep, 5)
print('깼당!')
async def 재우기_5번호출():
futures = []
for i in range(5):
futures.append(asyncio.ensure_future(쿨쿨()))
await asyncio.gather(*futures)
std = datetime.datetime.now()
print(f'Started at {std}')
loop = asyncio.get_event_loop()
loop.run_until_complete(재우기_5번호출())
loop.close()
edd = datetime.datetime.now()
print(f'Ended at {edd}')
print(f'Total time = {edd - std}')
Started at 2022-07-02 18:35:58.227417
깼당!
깼당!
깼당!
깼당!
깼당!
Ended at 2022-07-02 18:36:03.231902
Total time = 0:00:05.004485
쿨쿨 함수를 5번을 호출했음에도 5.004 초 밖에 걸리지 않았습니다.
그럼 조금 더 자세히 보겠습니다.
loop = asyncio.get_event_loop()
loop.run_until_complete(재우기_5번호출())
loop.close()
https://docs.python.org/ko/3/library/asyncio-eventloop.html
get_event_loop() : 현재의 이벤트 루프를 가져옵니다. get_running_loop() 와 달리 loop 가 존재하지 않을 경우 예외가 나오지 않고 새로운 이벤트 루프를 생성하여 줍니다. 3.10 버전부터는 get_or_create 기능이 deprecated 된다고 있으니 조심하세요!
run_until_complete(재우기_5번호출()) : 현재 이벤트 루프에서 coroutin 함수를 호출하고 Future 의 인스턴스의 결과를 반환 혹은 예외를 일으킵니다. 여기서 Future의 인스턴스는 재우기_5번호출 함수이며 이는 코루틴 객체 이기에 asyncio.task 로 실행되도록 묵시적으로 예약됩니다.
재우기_5번호출 함수는 엄격한 future 가 아닙니다!
asyncio.isfuture(재우기_5번호출()) == False
문서의 ensure_future 에서는 coroutine 일 경우 isfuture 대신 iscoroutine 을 사용해 검사한다고 있으며 이는 하기 코드의 원리와 같습니다.
asyncio.isfuture(asyncio.Tasks(재우기_5번호출())) == True
이제 드디어 우리의 이벤트 루프에서 native coroutine 인 재우기_5번호출 함수가 호출되었습니다.
async def 재우기_5번호출():
futures = []
for i in range(5):
futures.append(asyncio.ensure_future(쿨쿨()))
await asyncio.gather(*futures)
위 사진과 같이 ensure_futre 는 인자로 받은 코루틴 쿨쿨을 Task 로 wrapping 한 결과를 반환합니다.
print(asyncio.ensure_future(쿨쿨()))
====================================
<Task pending name='Task-3' coro=<쿨쿨() running at testtest.py:4>>
ensure_futre 로 반환받은 Task 들을 futures 리스트에 저장한 후 gather 함수를 통해 한번에 실행시켜 줍니다.
async def 재우기():
futures, futures_a, futures_b = [], [], []
for i in range(5):
futures.append(asyncio.ensure_future(쿨쿨()))
futures_a.append(asyncio.ensure_future(쿨쿨()))
futures_b.append(asyncio.ensure_future(쿨쿨()))
g1 = asyncio.gather(*futures)
g2 = asyncio.gather(*futures_a)
g3 = asyncio.gather(*futures_b)
await asyncio.gather(g1, g2, g3)
이제 가장 내부에 있는 쿨쿨() 함수입니다.
async def 쿨쿨():
await loop.run_in_executor(None, time.sleep, 5)
print('깼당!')
run_in_executor 은 코루틴 함수가 아닌 함수를 코루틴으로 만들어줍니다. 만약 그냥 time.sleep(5) 을 호출한다면 실행시간은 25초가 됩니다. 코루틴 함수 안에서 실행하는 함수 또한 코루틴이어야하기 때문입니다. 쿨쿨 함수는 다음과 같이 사용할 수도 있습니다.
async def 쿨쿨():
await asyncio.sleep(5)
print('깼당!')
asyncio 에서 제공하는 sleep 은 그 자체가 코루틴 함수이며 이는 논블로킹 함수이기에 그냥 사용해도 되지만 time 모듈의 sleep 은 블로킹 함수여서 코루틴으로 만들어 주는 과정이 필요합니다. 우리가 자주 사용하는 대부분의 python 모듈 (bs4, request ...) 모두 코루틴 함수가 아니기 때문에 이에 대응되는 asyncio 에서 제공하는 모듈을 사용하거나 run_in_excutor 를 통해 코루틴 함수로 만들어 줄 필요가 있는 것이죠!
이 같은 과정들을 통해 비동기 로직을 구현해 보았습니다. async, await 는 node.js 와 비슷하고 future는 node.js 의 Promise 와 결이 비슷하다는 생각이 드네요.
작은 서비스 혹은 기능이나 aws lambda 와 같이 별도로 구축해 놓은 모듈에는 asyncio 를 사용하였지만 아직 코어 서비스에는 적용해본 부분이 없습니다. 코어 서비스에서 어떤 식으로 네이티브 코루틴이 사용될 수 있을지 고민해봐야겠네요.