Async/Await
Interroga decine di API in parallelo invece che in sequenza: da 100 secondi a 3 con la programmazione asincrona di Python.
Async/Await
Lo studio tecnico dell'Ing. Paolo Ferretti ha un incarico: raccogliere i dati urbanistici da 50 comuni diversi per una valutazione ambientale. Ogni comune espone un'API REST, e ogni richiesta impiega circa 2 secondi. In modo sequenziale, il programma impiegherebbe 100 secondi. Con la programmazione asincrona, tutte le richieste partono in parallelo e il risultato arriva in 2-3 secondi.
Il problema: richieste sequenziali lente
Ecco cosa succede senza async. Il programma resta bloccato ad aspettare ogni risposta prima di inviare la richiesta successiva.
import time
def richiedi_dati_comune(comune):
"""Simula una richiesta API che impiega 2 secondi."""
print(f"Richiedo dati da {comune}...")
time.sleep(2) # simula attesa di rete
return {"comune": comune, "abitanti": 15000, "zona": "B2"}
# 50 comuni in sequenziale: 50 x 2s = 100 secondi!
comuni = [f"Comune_{i}" for i in range(50)]
inizio = time.time()
risultati = []
for comune in comuni:
dati = richiedi_dati_comune(comune)
risultati.append(dati)
durata = time.time() - inizio
print(f"Completato in {durata:.1f}s") # Completato in 100.0sIl problema è chiaro: mentre aspettiamo la risposta di un comune, il programma non fa assolutamente nulla. È tempo sprecato.
La soluzione async
Con asyncio, le richieste partono tutte insieme. Mentre aspettiamo la risposta del Comune 1, il programma invia la richiesta al Comune 2, 3, 4 e così via.
import asyncio
async def richiedi_dati_comune(comune):
"""Versione asincrona: non blocca durante l'attesa."""
print(f"Richiedo dati da {comune}...")
await asyncio.sleep(2) # simula attesa I/O senza bloccare
return {"comune": comune, "abitanti": 15000, "zona": "B2"}
async def main():
comuni = [f"Comune_{i}" for i in range(50)]
# Tutte le richieste partono in parallelo
risultati = await asyncio.gather(
*[richiedi_dati_comune(c) for c in comuni]
)
print(f"Ricevuti dati da {len(risultati)} comuni")
import time
inizio = time.time()
asyncio.run(main())
durata = time.time() - inizio
print(f"Completato in {durata:.1f}s") # Completato in 2.0sDa 100 secondi a 2. La differenza è abissale.
Passo per passo: async def, await, asyncio.run()
Tre concetti fondamentali:
- ●`async def` dichiara una funzione asincrona (detta coroutine)
- ●`await` mette in pausa la coroutine mentre aspetta il risultato, liberando il loop per fare altro
- ●`asyncio.run()` avvia il loop degli eventi e esegue la coroutine principale
# 1. Definisci la coroutine
async def scarica_documento(codice):
print(f"Scaricamento documento {codice}...")
await asyncio.sleep(1) # durante questa attesa, il loop può fare altro
return f"Contenuto del documento {codice}"
# 2. Crea la funzione principale asincrona
async def main():
documento = await scarica_documento("URB-2024-001")
print(documento)
# 3. Avvia il tutto
asyncio.run(main())Batch di richieste con asyncio.gather
asyncio.gather() è il cuore della concorrenza. Accetta un numero qualsiasi di coroutine e le esegue contemporaneamente, restituendo i risultati nello stesso ordine.
async def analisi_multi_comune():
"""Raccoglie dati urbanistici da più comuni in parallelo."""
comuni_target = ["Milano", "Roma", "Napoli", "Torino", "Bologna"]
# Crea le coroutine
tasks = [richiedi_dati_comune(comune) for comune in comuni_target]
# Esegui tutte in parallelo
risultati = await asyncio.gather(*tasks)
# Elabora i risultati
for dati in risultati:
print(f"{dati['comune']}: zona {dati['zona']}, {dati['abitanti']} abitanti")
asyncio.run(analisi_multi_comune())Richieste HTTP reali con aiohttp
Per fare richieste HTTP reali in modo asincrono, serve la libreria aiohttp (installabile con pip install aiohttp). La libreria standard requests è sincrona e blocca il loop.
import aiohttp
import asyncio
async def scarica_dati_comune(session, url_comune):
"""Scarica i dati urbanistici da un singolo comune."""
async with session.get(url_comune) as risposta:
dati = await risposta.json()
return dati
async def raccogli_dati_urbanistici():
"""Interroga 50 API di comuni in parallelo."""
base_url = "https://api.comuni.example.it/urbanistica"
urls = [f"{base_url}?comune={i}" for i in range(50)]
async with aiohttp.ClientSession() as session:
tasks = [scarica_dati_comune(session, url) for url in urls]
risultati = await asyncio.gather(*tasks)
print(f"Raccolti dati da {len(risultati)} comuni")
return risultati
asyncio.run(raccogli_dati_urbanistici())La ClientSession gestisce il pool di connessioni e va riutilizzata per tutte le richieste della stessa sessione.
Semafori per rate limiting
Lanciare 50 richieste simultanee potrebbe sovraccaricare il server del comune e farci bloccare l'IP. Un semaforo limita quante richieste possono essere attive contemporaneamente.
async def scarica_con_limite(session, url, semaforo):
"""Scarica rispettando il limite di connessioni simultanee."""
async with semaforo:
# Massimo N richieste alla volta
async with session.get(url) as risposta:
return await risposta.json()
async def raccogli_con_rate_limit():
"""Interroga 50 comuni, massimo 10 richieste alla volta."""
semaforo = asyncio.Semaphore(10) # max 10 contemporanee
base_url = "https://api.comuni.example.it/urbanistica"
urls = [f"{base_url}?comune={i}" for i in range(50)]
async with aiohttp.ClientSession() as session:
tasks = [scarica_con_limite(session, url, semaforo) for url in urls]
risultati = await asyncio.gather(*tasks)
print(f"Completate {len(risultati)} richieste con rate limiting")
return risultati
asyncio.run(raccogli_con_rate_limit())Il semaforo funziona come un contatore: al massimo 10 coroutine possono entrare nel blocco async with semaforo contemporaneamente. Le altre aspettano il loro turno.
Da ricordare
- ●Usa
async/awaitquando il programma aspetta risposte di rete, file o database (operazioni I/O-bound) - ●
asyncio.gather()esegue più coroutine in parallelo e restituisce i risultati ordinati - ●Per le richieste HTTP asincrone serve
aiohttp, nonrequests - ●Usa
asyncio.Semaphoreper limitare le richieste contemporanee e non sovraccaricare i server - ●La programmazione asincrona non velocizza i calcoli CPU-intensive — per quelli serve
multiprocessing - ●Ogni funzione
asyncdeve essere chiamata conawaito passata aasyncio.gather()