[Python] 비동기 프로그래밍 정리 4 (create_task 함수)

hodu·2022년 11월 6일
0

asyncio

목록 보기
4/7
post-thumbnail

아래 링크를 읽고 정리한 내용입니다.

🚨 더 구체적인 내용은 아래 링크에 아주 친절하게 잘 정리되어 있으니 꼭 링크가서 읽어주세요!!
https://it-eldorado.tistory.com/159?category=749661


이전 글에서는 이벤트 루프가 태스크들을 "동시적"으로 실행한다고 설명하였다.
그러나 사실 asyncio.run()함수는 기본적으로 하나의 태스크만을 생성하여 실행한다.
따라서 코루틴 체인 과정에서 추가적인 태스크를 생성하여 실행하지 않았다면 현재의 태스크가 중단되었을 때 이벤트 루프는 실행시킬 다른 태스크가 없게 된다.

태스크가 한 개라면 동시적인 실행을 하는 것이 애초에 말이 되지 않는 것이다.

따라서 동시적인 실행을 위해서는 asyncio.create_task()함수를 호출함으로써 태스크를 추가로 생성하여 실행해야 한다.

이 함수를 호출할 때 코루틴 객체를 인자로 넘기면 해당 코루틴 객체를 이용하여 태스크 객체를 생성하고 이를 반환한다.
그리고 앞서 말했듯 태스크 객체가 생성되면 해당 태스크 객체가 나타내는 태스크의 실행이 이벤트 루프에 의해 즉시 에약된다.(즉시 실행이 아니다.)단, 이함수는 3.7 버전 이상의 Python에서만 사용할 수 있기 때문에, 그 이전 버전에서는 asyncio.ensure_future()함수를 사용해야 한다.

다음으로, 모든 퓨처 객체(태스크 객체를 포함한다.)들이 완료 상태가 될 때까지 기다리는 함수가 asyncio.gather()이다. 이 함수는 인자로 여러개의 Awaitable한 객체들을 받을 수 있는데, 만약 코루틴 객체를 받으면 이는 자동으로 태스크 객체로 래핑이 된다. 따라서 사실상 퓨처 객체(태스크 객체 포함)만 넘어간다고 생각해도 된다.
그리고 모든 퓨처 객체들이 완료상태가 되면, 그것들의 결과 값을 "리스트 형태"로 반환한다.
그 순서는 인자로 넘긴 순서와 동일하다. 이 함수는 await키워드 뒤에서 호출될 수 있는 코루틴의 일종이다.

예시 코드를 통해 한 번 알아보자.

import asyncio
import time

async def sleep(sec):
    await asyncio.sleep(sec) 
    return sec

async def main():
    sec_list = [1, 2]
    tasks = [asyncio.create_task(sleep(sec)) for sec in sec_list]  
    tasks_results = await asyncio.gather(*tasks)
    return tasks_results

start = time.time()

loop = asyncio.get_event_loop()
result = loop.run_until_complete(main())
loop.close()

end = time.time()

print('result : {}'.format(result))
print('total time : {0:.2f} sec'.format(end - start))

# 출력 결과
# result : [1, 2]
# total time : 2.00 sec

위 예시코드의 실행흐름을 파악해보면 다음과 같다.

  1. loop.run_until_complete() 함수를 통해 Task0이 실행이 되고 이로 인해 main() 코루틴이 예약된다.
  2. main() 코루틴은 asyncio.create_task() 함수를 통해 (sec_list가 총 2개이니)Task1, Task2 객체를 생성하고 실행을 예약한다.
  3. asyncio.gather() 코루틴은 Task1 객체를 await한다.
  4. Task0 은 Task1 객체가 완료될 때 까지 기다리도록 하고 이벤트 루프에게 제어를 넘긴다.
  5. 이벤트 루프가 Task1을 실행한다.
  6. Task1 은 sleep(1) 코루틴을 실행하고, 인자값을 받아 sleep 함수 내 asyncio.sleep(1) 코루틴을 실행한다.
  7. asyncio.sleep(1) 코루틴은 Future1 객체를 만들고 1초 뒤에 Future1 객체의 결과 값이 갱신되도록 이벤트 루프에 예약을 건 뒤 Future1 객체를 await한다.
  8. Task1은 Future1 객체가 완료 상태가 될 때까지 기다리도록 하고, 이벤트루프에게 제어권을 넘긴다.
  9. 이벤트 루프가 Task2 를 실행한다.
  10. Task2 는 sleep(2) 코루틴을 시랳ㅇ하고, 다시 asyncio.sleep(2) 코루틴을 실행한다.
  11. asyncio.sleep(2) 코루틴은 Future2 객체를 만들고 2초뒤에 Future2 객체의 결과 값이 갱신되도록 이벤트 루프에 예약을 건뒤 Future2 객체를 await한다.
  12. Task2 는 Future2 객체가 완료상태가 될 때까지 기다리도록 하고, 이벤트 루프에게 제어를 넘긴다.
  13. 이제 이벤트 루프는 실행할 태스크가 없으므로 아무것도 하지 않는다.
  14. 그러다가 1초가 지나면 이벤트 루프는 Future1 객체의 결과 값을 갱신한다.
    이로인해 Future1 객체가 완료상태가 될 때까지 기다리던 Task1의 실행이 다시 예약된다.
  15. 이벤트 루프가 Task1 을 실행한다.
  16. asyncio.sleep(1) 코루틴으로 돌아가서 실행이 중단되었던 부분부터 실행을 재개한다.
  17. asyncio.sleep(1) 코루틴이 리턴하고, sleep(1) 코루틴도 리턴한다. 이때 반환 값은 1이다.
  18. Task1 객체의 결과 값이 1로 설정되면서 Task1의 실행이 완료된다 이로 인해 Task1 객체가 완료 상태가 될 때까지 기다리던 Task0의 실행이 다시 예약된다.
  19. 이벤트 루프가 Task0 을 실행한다.
  20. asyncio.gather() 코루틴으로 돌아가서 실행이 중단되었던 부분부터 실행을 재개한다.
  21. asyncio.gather() 코루틴은 Task1 객체의 결과 값을 저장하고, Task2 객체를 await 한다.
  22. Task0은 Task2 객체가 완료 상태가 될 때까지 기다리도록 하고, 이벤트 루프에게 제어를 넘긴다.
  23. 이제 이벤트 루프는 실행할 태스크가 없으므로 아무것도 하지 않는다.
  24. 14~20 반복
  25. asycnio.gather() 코루틴은 [Task1 객체의 결과 값, Task 2 객체의 결과 값], 즉 [1, 2]를 리턴한다.
  26. main() 코루틴도 리턴한다. 이때 반환 값은 [1, 2]이다.
  27. Task0 객체의 결과값이 [1, 2]로 설정되면서 Task0 의 실행이 완료된다.
  28. loop.run_until_complete()의 실행이 완료되고 이벤트 루프를 닫는다.
profile
안녕 세계!

0개의 댓글