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을 취소합니다. 타임아웃과 취소 페이지는 취소가 호출 체인을 통해 어떻게 전파되는지 자세히 다룹니다.