8.4. Gather#

이전 장에서는 asyncio.create_task() 가 코루틴을 스케줄링하면서도 그것을 기다리지 않는다는 것을 보여주었습니다. 짝이 되는 연산 – 여러 awaitable을 동시에 실행하고 그것들 모두를 기다리는 것 – 은 asyncio.gather() 입니다.

8.4.1. 기본 형태#

import asyncio

async def fetch(name, delay_ms):
    await asyncio.sleep_ms(delay_ms)
    return name

async def main():
    results = await asyncio.gather(
        fetch("a", 100),
        fetch("b", 200),
        fetch("c", 50),
    )
    print(results)

asyncio.run(main())

출력:

['a', 'b', 'c']

두 가지를 주목하세요. 첫째, 결과 리스트는 코루틴이 완료된 순서가 아니라 gather에 전달된 인자의 순서입니다 – "c" 가 먼저 반환되었지만 여전히 세 번째 항목입니다. 둘째, 호출은 350 ms 가 아니라 총 200 ms 가 걸렸습니다: 세 개의 sleep이 동시에 실행되었고, gather() 는 가장 느린 것이 완료되는 즉시 반환합니다.

두 사실 모두 같은 원천에서 나옵니다. gather() 는 아직 작업이 아닌 각 인자(예제의 코루틴들)를 감싸서 루프에 스케줄링하고, 그것들이 모두 완료될 때까지 호출하는 코루틴을 일시 중단한 다음, 원래 순서대로 결과를 반환합니다.

8.4.2. 언제 사용해야 하는가#

애플리케이션이 N개의 awaitable 연산을 가지고 있고 계속하기 전에 그것들 모두의 결과를 원하는 모든 경우입니다. 전형적인 예:

  • 여러 네트워크 요청을 병렬로 발행하고 모든 응답을 기다리기.

  • 결합된 결과를 처리하기 전에 여러 센서로부터 동시에 읽기.

  • 애플리케이션 단계의 끝에서 수명이 짧은 여러 헬퍼 작업을 합치기.

애플리케이션이 시작하여 프로그램 수명 동안 계속 실행되도록 놔두는 장기 실행 백그라운드 작업에는 적합한 도구가 아닙니다 – 그것들은 여전히 create_task() 의 일입니다. gather()팬아웃 / 팬인 패턴을 위한 것입니다: 작업을 나누고, 동시에 수행하고, 다시 합칩니다.

8.4.3. 그룹 내의 예외#

수집된 awaitable 중 하나라도 예외를 발생시키면, 기본 동작은 gather 호출 밖으로 그 예외를 다시 발생시키는 것입니다. 아직 완료되지 않은 형제들은 백그라운드에서 취소됩니다.

그것은 보통 애플리케이션이 원하는 바입니다 – N개의 병렬 작업 중 하나가 실패했으니 결합된 연산이 실패한 것이고, 따라서 나머지에 시간을 쓰는 것을 멈춥니다. 때로는 애플리케이션이 반대를 원합니다: 각 awaitable이 독립적으로 완료(또는 실패)하도록 두고, 이후에 결과를 검사하는 것입니다. 그것을 위해서는 return_exceptions=True 를 전달하세요:

results = await asyncio.gather(
    fetch_or_fail("ok"),
    fetch_or_fail("bad"),
    return_exceptions=True,
)
# results == ["ok-value", OSError(...)]

이제 반환된 리스트의 각 항목은 정상적인 반환값 또는 해당 awaitable이 발생시킨 예외 중 하나입니다. 호출자는 isinstance(r, Exception) 을 확인하여 둘을 구별합니다.

8.4.4. 취소#

gather 자체를 취소하는 것 – 그것을 대기 중이던 작업을 취소함으로써 – 은 그 안에서 아직 실행 중인 모든 awaitable을 취소합니다. 타임아웃과 취소 페이지는 취소가 호출 체인을 통해 어떻게 전파되는지 자세히 다룹니다.