프로그래밍/Python

[python] asyncio.gather() 함수와 asyncio.wait() 함수의 차이는?

채윤아빠 2025. 6. 26. 09:27

asyncio.gather()asyncio.wait()는 모두 여러 코루틴(또는 태스크)을 동시에 실행하고 그 결과를 기다리는 데 사용되는 asyncio 라이브러리의 핵심 함수입니다. 하지만 동작 방식과 사용 목적에 있어서 중요한 차이점이 있습니다.

asyncio.gather(*aws, return_exceptions=False)

  • 목적: 여러 개의 awaitable 객체(코루틴, 태스크, 퓨처 등)를 그룹으로 묶어 함께 실행하고, 모든 awaitable이 완료될 때까지 기다린 후 그 결과를 수집합니다.
  • 반환 값: 모든 awaitable의 결과가 담긴 리스트를 반환합니다. 이 리스트의 순서는 입력된 awaitable의 순서와 동일합니다.
  • 예외 처리
    • 기본적으로 return_exceptions=False입니다. 이 경우, 하나의 awaitable이라도 예외를 발생시키면 gather()는 즉시 나머지 실행 중인 awaitable을 취소하고 해당 예외를 발생시킵니다.
    • return_exceptions=True로 설정하면, 예외가 발생하더라도 gather()는 다른 awaitable의 실행을 계속 진행하고, 예외 객체를 결과 리스트에 포함시킵니다.
  • 사용 시점
    • 모든 태스크가 성공적으로 완료되어야 다음 단계로 진행할 수 있을 때.
    • 모든 태스크의 결과를 한 번에 얻고 싶을 때.
    • 데이터베이스 쿼리, 여러 API 호출, 파일 다운로드 등 독립적인 여러 작업을 동시에 수행하고 모든 결과를 취합해야 할 때 유용합니다.

예시

import asyncio

async def func1():
    await asyncio.sleep(1)
    print("func1 완료")
    return "결과1"

async def func2():
    await asyncio.sleep(2)
    print("func2 완료")
    return "결과2"

async def main_gather():
    print("gather 시작")
    results = await asyncio.gather(
        func1(),
        func2()
    )
    print(f"gather 결과: {results}")

if __name__ == "__main__":
    asyncio.run(main_gather())

"""실행결과 >>>
gather 시작
func1 완료
func2 완료
gather 결과: ['결과1', '결과2']
"""

asyncio.wait(aws, *, timeout=None, return_when=ALL_COMPLETED)

  • 목적: 여러 개의 awaitable 객체(주로 태스크)를 실행하고, 특정 조건(return_when)이 충족될 때까지 기다린 후 완료된 태스크와 아직 실행 중인 태스크를 분리하여 반환합니다.
  • 반환 값: (done, pending) 튜플을 반환합니다.
    • done: 조건이 충족되어 완료된 태스크들의 집합 (set)
    • pending: 아직 완료되지 않고 실행 중인 태스크들의 집합 (set)
  • return_when 인자
    • asyncio.ALL_COMPLETED (기본값): 모든 태스크가 완료될 때까지 기다립니다. pending 집합은 비어있게 됩니다. (이 경우 gather와 유사한 동작을 보일 수 있지만, 결과 반환 방식과 예외 처리 방식은 다릅니다.)
    • asyncio.FIRST_COMPLETED: 태스크 중 하나라도 완료되면 즉시 반환합니다.
    • asyncio.FIRST_EXCEPTION: 태스크 중 하나라도 예외를 발생시키면 즉시 반환합니다. 예외가 발생하지 않으면 ALL_COMPLETED처럼 모든 태스크가 완료될 때까지 기다립니다.
  • 예외 처리: wait() 자체는 태스크에서 발생한 예외를 직접적으로 발생시키지 않습니다. 예외가 발생한 태스크도 done 집합에 포함되며, 해당 태스크의 result() 메서드를 호출하거나 await 할 때 예외가 다시 발생합니다. (이것이 위에 제시된 asyncio.CancelledError 처리 예제에서 await task를 하는 이유입니다.)
  • timeout 인자: 지정된 시간(초) 내에 return_when 조건이 충족되지 않으면 함수가 반환됩니다. 이 경우 완료되지 않은 태스크는 pending 집합에 포함됩니다.
  • 사용 시점
    • 가장 빠른 태스크의 결과를 얻고 싶을 때 (예: 여러 웹 서비스 중 가장 먼저 응답하는 곳의 데이터를 사용).
    • 특정 시간 제한 내에 작업이 완료되기를 기다리고, 그 이후에는 나머지 작업을 취소하거나 다른 처리를 하고 싶을 때.
      • 태스크의 완료 여부를 세밀하게 제어해야 할 때.

예시

import asyncio

async def func3():
    await asyncio.sleep(3)
    print("func3 완료")
    return "결과3"

async def func4():
    await asyncio.sleep(1)
    print("func4 완료")
    return "결과4"

async def main_wait():
    print("wait 시작 (FIRST_COMPLETED)")
    task3 = asyncio.create_task(func3())
    task4 = asyncio.create_task(func4())

    done, pending = await asyncio.wait(
        [task3, task4],
        return_when=asyncio.FIRST_COMPLETED
    )

    print(f"wait 결과: done={done}, pending={pending}")

    # 완료되지 않은 태스크 취소 (예시에서처럼 직접 처리해야 함)
    for task in pending:
        task.cancel()
        try:
            await task
        except asyncio.CancelledError:
            print(f"{task.get_name()} 취소됨")

if __name__ == "__main__":
    asyncio.run(main_gather())

"""실행결과 >>>
wait 시작 (FIRST_COMPLETED)
func4 완료
wait 결과: done={<Task finished name='Task-3' coro=<func4() done, defined at D:\MyProj\python-test\general\asyncio\wait-example:8> result='결과4'>}, pending={<Task pending name='Task-2' coro=<func3() running at D:\MyProj\python-test\general\asyncio\wait-example:4> wait_for=<Future pending cb=[Task.task_wakeup()]>>}
Task-2 취소됨
"""

주요 차이점 요약

특징 asyncio.gather() asyncio.wait()
목적 모든 awaitable이 완료될 때까지 기다리고 결과 수집 특정 조건(return_when)에 따라 반환하며, 완료/대기 태스크 분리
반환 값 결과 리스트 (done, pending) 튜플 (태스크 집합)
예외 처리 기본적으로 첫 예외 발생 시 모든 태스크 취소 및 예외 발생 예외를 직접 발생시키지 않음; done 태스크의 result() 호출 시 예외 발생
유연성 단순 그룹 실행 및 결과 취합에 적합 완료 조건(return_when, timeout)을 세밀하게 제어 가능
인자 *aws (awaitables), return_exceptions aws (awaitables), timeout, return_when
주요 사용처 모든 서브 작업이 완료되어야 할 때 가장 빠른 작업의 결과가 필요하거나, 타임아웃 처리가 필요할 때

맺는말

asyncio.gather()는 "모든 것을 동시에 실행하고, 모든 결과가 필요해!"라는 경우에 적합하고, asyncio.wait()는 "이것들 중에 뭐라도 먼저 끝나거나, 일정 시간 내에 뭐가 되는지 보고 싶어!"라는 경우에 더 적합합니다. Python 3.11부터는 asyncio.TaskGroup이라는 더 안전하고 구조화된 동시성 API도 도입되어, 많은 경우 gatherwait의 대안으로 사용될 수 있습니다.



728x90
반응형