파이썬 동기 & 비동기 처리

홍찬우·2023년 7월 30일

asyncio

await

  • 비동기 함수가 바로 리턴 해버리면서 완료 되지 않은 결과에 접근하는 것을 방지하기 위해
    비동기 함수 호출 앞에 await 키워드를 사용

  • await 키워드를 사용하면 요청 했던 비동기 작업의 완료 통보가 올 때까지 대기

    • 그런데 아무것도 하지 않으면서 기다리는 것이 아니고 event loop를 먼저 확인하고,
      event loop에 일거리가 있다면 그 일들을 처리하면서 기다림
  • 다음으로 바로 진행하지 않고 완료 통보가 올 때 까지 진행

    • 이벤트 루프에 일거리가 있다면 해당 작업들을 처리하면서 대기
import asyncio
import time

async def get_num(num):
    await asyncio.sleep(5)
    print(f"{num} 접근")
    
async def main() :  
    await get_num(1)
    await get_num(2)
    await get_num(3)
    await get_num(4)
    await get_num(5)
    await get_num(6)

asyncio.run(main())  # main 함수 실행
  • 위 코드는 비동기로 처리되지 않으며, 매 task 다 5초 sleep이 실행되어 5*6=30초 이상 걸린다.

  • 이는 event loop에 6개의 task가 등록되지 않아서 다 따로 처리된 것임

    • 위 설명처럼 await은 event loop에 일거리가 있으면 처리하면서 기다림

import asyncio
import time

async def get_num(num):
    await asyncio.sleep(5)
    print(f"{num} 접근")
    
    
async def main() :
    # using create_task
		task1 = asyncio.create_task(get_num(1))
    task2 = asyncio.create_task(get_num(2))
    task3 = asyncio.create_task(get_num(3))
    task4 = asyncio.create_task(get_num(4))
    task5 = asyncio.create_task(get_num(5))
    task6 = asyncio.create_task(get_num(6))
    
    await task1
    await task2
    await task3
    await task4
    await task5
    await task6

asyncio.run(main())  # main 함수 실행
  • 위 코드는 5초에 모든 task가 완료 된다.

  • create_task() 를 이용해 모든 task를 event loop에 등록

  • 실행 순서는 1, 3, 6, 5, 2, 4이며, 비동기 처리로 실행 순서는 보장되지 않는다.

  • create_task() 말고 asyncio.gather() 사용해도 동일

    # using gather
        await asyncio.gather(
            get_num(1),
            get_num(2),
            get_num(3),
            get_num(4),
            get_num(5),
            get_num(6)
        )

만약 await asyncio.sleep(5)time.sleep(5)여도 비동기로 처리될까?

→ time.sleep(5)는 동기로 처리되며, 바꿨을 경우 5*6=30초의 실행 시간이 걸리게 된다.


그렇다면 get_num() 함수에서 print를 2번 한다면, 결과는 어떨까?

import asyncio
import time

async def get_num(num):
    await asyncio.sleep(5)
    print(f"{num} 1번 째로 접근")
    print(f"{num} 2번 째로 접근")

async def main() :
    # using create_task
    task1 = asyncio.create_task(get_num(1))
    task2 = asyncio.create_task(get_num(2))
    task3 = asyncio.create_task(get_num(3))
    task4 = asyncio.create_task(get_num(4))
    task5 = asyncio.create_task(get_num(5))
    task6 = asyncio.create_task(get_num(6))
    
    await task1
    await task2
    await task3
    await task4
    await task5
    await task6

asyncio.run(main())  # main 함수 실행
1 1번 째로 접근
1 2번 째로 접근
3 1번 째로 접근
3 2번 째로 접근
6 1번 째로 접근
6 2번 째로 접근
5 1번 째로 접근
5 2번 째로 접근
2 1번 째로 접근
2 2번 째로 접근
4 1번 째로 접근
4 2번 째로 접근
  • 처음엔 task 1~6과 각 task 내부에 존재하는 2개의 print가 모두 비동기로 처리 돼 뒤죽박죽 나올 것을 예상

  • print는 동기 함수이기 때문에 순차적으로 출력된 것

  • 만약 get_num 내부 함수가 print가 아니라 다른 task고, 그 안에 또 다른 event loop이 만들어 진다면,

    task1의 get_num() 내부 함수들이 비동기로 처리되고 → 모두 마치면 task2로 넘어갈 것

    ⇒ 즉 비동기 처리 함수 내부의 비동기 처리를 원하는 만큼 깊게 쌓을 수 있음


get_running_loop & run_in_executor

  • get_running_loop

    • get_event_loop 과 비슷하나 해당 함수는 파이썬 3.10 이상부터 deprecate

    • 현재 실행 중인 이벤트 루프를 반환

  • run_in_executor(executor, func, *args)

    • 지정된 executor에서 func가 호출되도록 등록

      • executor=None이면 기본 executor가 사용
    • *args는 func 함수에 사용될 인자를 의미

    • 동기 함수인 func를 비동기로 처리할 수 있게 해줌


import asyncio
import time

async def get_num(num):
    await asyncio.sleep(5)
    time.sleep(5)
    print(f"{num} 접근")
    

async def main() :
    # using create_task
    task1 = asyncio.create_task(get_num(1))
    task2 = asyncio.create_task(get_num(2))
    task3 = asyncio.create_task(get_num(3))
    task4 = asyncio.create_task(get_num(4))
    task5 = asyncio.create_task(get_num(5))
    task6 = asyncio.create_task(get_num(6))
    
    await task1
    await task2
    await task3
    await task4
    await task5
    await task6

asyncio.run(main())  # main 함수 실행
  • 위 코드는 task 1~6은 비동기로 실행 되나, time.sleep(5) 가 동기로 처리되어
    task 1~6을 비동기로 처리하는 시간 5초 + 각 task의 time.sleep(5) 5*6=30초 = 35초가 걸린다.

  • 실행 결과는 위와 동일하게 1, 3, 6, 5, 2, 4가 출력

import asyncio
import time

async def get_num(num):
    await asyncio.sleep(5)
    loop = asyncio.get_running_loop()  # 이벤트 루프 객체 얻기
    await loop.run_in_executor(None, time.sleep, 5)
    print(f"{num} 접근")
    

async def main() :
    # using create_task
    task1 = asyncio.create_task(get_num(1))
    task2 = asyncio.create_task(get_num(2))
    task3 = asyncio.create_task(get_num(3))
    task4 = asyncio.create_task(get_num(4))
    task5 = asyncio.create_task(get_num(5))
    task6 = asyncio.create_task(get_num(6))
    
    await task1
    await task2
    await task3
    await task4
    await task5
    await task6

asyncio.run(main())  # main 함수 실행
  • 동기 함수인 time.sleeprun_in_executor 이용해 비동기로 처리

  • 실행 시간은 task 1~6 비동기 처리 5초 + sleep 비동기 처리 5초 = 10초

  • 실행 결과는 위와 다르게 1, 3, 6, 5 ,2, 4가 아니며, 실행 할 때마다 매번 다른 순서로 출력

⇒ 실행 시간이 긴 동기 함수를 비동기로 처리하고 싶을 때 (e.g., requests.get(url)) 사용

profile
AI-Kid

0개의 댓글