8.4. Gather¶
Il capitolo precedente ha mostrato come asyncio.create_task() schedula una coroutine e non la attende. L’operazione complementare – eseguire più awaitable in modo concorrente e attenderle tutte – è asyncio.gather().
8.4.1. La forma di base¶
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())
Output:
['a', 'b', 'c']
Due cose da notare. Primo, la lista dei risultati segue l’ordine degli argomenti passati a gather, non l’ordine in cui le coroutine sono terminate – "c" è stata restituita per prima ma rimane comunque la terza voce. Secondo, la chiamata ha impiegato 200 ms in totale, non 350 ms: i tre sleep sono stati eseguiti in modo concorrente, e gather() ritorna non appena il più lento di essi è completato.
Entrambi i fatti derivano dalla stessa origine. gather() incapsula ogni argomento che non sia già un task (le coroutine nell’esempio), li schedula sul loop, sospende la coroutine chiamante finché tutti non sono terminati, poi restituisce i loro risultati nell’ordine originale.
8.4.2. Quando ricorrervi¶
Ogni volta che l’applicazione ha N operazioni awaitable e vuole i risultati di tutte prima di continuare. Esempi tipici:
Inviare più richieste di rete in parallelo e attendere tutte le risposte.
Leggere da più sensori in modo concorrente prima di elaborare il risultato combinato.
Unire più task ausiliari di breve durata al termine di una fase dell’applicazione.
Non è lo strumento giusto per i task di background a lunga durata che l’applicazione avvia e lascia in esecuzione per tutta la vita del programma – quelli rimangono compito di create_task(). gather() serve per il pattern fan-out / fan-in: suddividere il lavoro, eseguirlo in modo concorrente, ricongiungere.
8.4.3. Eccezioni nel gruppo¶
Se una delle awaitable raccolte solleva un’eccezione, il comportamento predefinito è ri-sollevare l’eccezione fuori dalla chiamata a gather. Le sorelle non ancora terminate vengono annullate in background.
Di solito è ciò che un’applicazione desidera – uno degli N job paralleli è fallito, quindi l’operazione combinata è fallita, quindi è inutile continuare a impiegare tempo sul resto. A volte l’applicazione vuole l’opposto: lasciare che ciascuna awaitable termini (o fallisca) in modo indipendente, ed esaminare i risultati in seguito. Per farlo si passa return_exceptions=True
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
Ora ogni voce della lista restituita è un normale valore di ritorno oppure l’eccezione sollevata dalla corrispondente awaitable. Il chiamante verifica isinstance(r, Exception) per distinguerle.
8.4.4. Annullamento¶
Annullare il gather stesso – annullando il task che lo stava attendendo – annulla ogni awaitable ancora in esecuzione al suo interno. La pagina timeout e annullamento illustra in dettaglio come l’annullamento si propaga lungo la catena delle chiamate.