Asyncio 실습
Asyncio의 이해
https://bentist.tistory.com/89
실습할 환경이 주피터 노트북이라면 아래 코드를 추가해줘야 실습이 가능하다. 주피터 노트북은 자체적인 이벤트 루프를 사용하고 있어서, 우리가 생성한 이벤트 루프와의 중첩을 피해야 한다.
!pip install nest_asyncio
import nest_asyncio
nest_asyncio.apply()
asyncio.sleep()
time.sleep()와 asyncio.sleep() 함수는 둘 다 의도적으로 1초의 지연 시간을 발생시킨다. 그러나 time.sleep() 함수는 현재 스레드를 1초 동안 정지시키지만, asyncio.sleep() 함수는 코루틴으로 구현되어 있기 때문에 현재 스레드를 정지시키지 않고 1초가 멈춘 사이 자동으로 이벤트 루프에 등록된 다른 코루틴이 실행될 수 있다. 의도적으로 지연 시간을 발생시키는 sleep() 함수는 사용자가 회원 가입을 하면 이메일로 인증 메일을 보내는 데 걸리는 시간과 같다. 만약 이를 동기 작업으로 구현하면 사용자는 서버가 인증 메일을 보내고 있는 작업 동안 아무것도 하지 못하고 기다려야 한다.
쉽게 말해 닭고기 샐러드를 요리할 때, 닭고기를 구우면서 요리 목록(이벤트 루프)에 야채 씻기(다른 코루틴)도 등록되어있다면 ⏳타이머를 맞춰놓고(asyncio.sleep()) 야채를 잠깐 씻으러 가는 것이다.
비동기로 두 개 이상의 작업(코루틴)을 실행할 때에는 asyncio.gather() 함수를 이용한다. 이때, 각 태스크들은 unpacked 형태로 넣어주어야 한다. 즉, asyncio.gather(co_1(), co_2()) 혹은 asyncio.gather(*[co_1(), co_2()])처럼 넣어야 한다.
import asyncio
import time
start = time.time()
async def async_sleep():
await asyncio.sleep(2)
async def co1():
print('함수 1 시작')
print('함수 1 중단, 2초간 대기')
print('') # 출력 결과를 더 보기 쉽도록 줄바꿈 코드 추가
await async_sleep()
print('함수 1 종료')
print('')
async def co2():
print('함수 2 시작')
print('함수 2 중단, 2초간 대기')
print('') # 출력 결과를 더 보기 쉽도록 줄바꿈 코드 추가
await async_sleep()
print('함수 2 종료')
print('')
async def co3():
print('함수 3 시작')
print('함수 3 중단, 2초간 대기')
print('') # 출력 결과를 더 보기 쉽도록 줄바꿈 코드 추가
await async_sleep()
print('함수 3 종료')
print('')
async def main():
tasks = [co1(), co2(), co3()]
await asyncio.gather(*tasks)
print(f"Completed after: {time.time() - start}")
asyncio.run(main())
>>>
함수 1 시작
함수 1 중단, 2초간 대기
함수 2 시작
함수 2 중단, 2초간 대기
함수 3 시작
함수 3 중단, 2초간 대기
함수 1 종료
함수 2 종료
함수 3 종료
Completed after: 2.0106050968170166
총 소요 시간을 보면 2초가 걸렸다. 2초의 대기시간 동안 스레드는 멈추지 않고 다른 함수를 실행하는 것이다. 만약 동기 함수인 time.sleep(2)였다면 첫번째 함수에서 스레드가 2초간 멈추고, 첫 번째 함수의 실행이 끝나야 두 번째 함수가 실행될 수 있다. 그럼 동기 방식에서는 총 6초(2 + 2 + 2)가 소요될 것이다.
import time
start = time.time()
def sync_sleep():
time.sleep(2)
def co1():
print('함수 1 시작')
print('함수 1 중단, 2초간 대기')
print('')
sync_sleep()
print('함수 1 종료')
print('')
def co2():
print('함수 2 시작')
print('함수 2 중단, 2초간 대기')
print('')
sync_sleep()
print('함수 2 종료')
print('')
def co3():
print('함수 3 시작')
print('함수 3 중단, 2초간 대기')
print('')
sync_sleep()
print('함수 3 종료')
print('')
def main():
co1()
co2()
co3()
print(f"Completed after: {time.time() - start}")
main()
>>>
함수 1 시작
함수 1 중단, 2초간 대기
함수 1 종료
함수 2 시작
함수 2 중단, 2초간 대기
함수 2 종료
함수 3 시작
함수 3 중단, 2초간 대기
함수 3 종료
Completed after: 6.024475812911987
여러 개의 비동기 함수 실행 순서
위의 비동기 함수 예제에서 두 개 이상의 작업(코루틴)을 asyncio.gather()로 등록하고, main() 함수에서 호출하였다. 그럼 이벤트 루프는 2개의 작업 중에서 어떤 작업을 먼저 실행하는 것일까?
asyncio.gather()에 등록된 순서대로 작업을 진행하고, 결과값 또한 등록된 순서대로 리스트로 반환한다.
import asyncio
import time
import nest_asyncio
nest_asyncio.apply()
start = time.time()
async def co1():
print('코루틴 1 시작')
await asyncio.sleep(3)
print('코루틴 1 재개')
return '3초 후 co1 종료'
async def co2():
print('코루틴 2 시작')
await asyncio.sleep(1)
print('코루틴 2 재개')
return '1초 후 co2 종료'
async def main():
tasks = [co1(), co2()]
# 동시에 2개 코루틴 예약(등록)
result = await asyncio.gather(*tasks)
print(f"Completed after: {time.time() - start}")
return result
asyncio.run(main())
>>>
코루틴 1 시작
코루틴 2 시작
코루틴 2 재개
코루틴 1 재개
Completed after: 3.0088226795196533
['3초 후 co1 종료', '1초 후 co2 종료']