Introduzione
Quando si lavora con asyncio in Python, non basta saper avviare coroutine o eseguire operazioni concorrenti: in applicazioni reali bisogna anche saperle interrompere in modo sicuro. Questo è fondamentale in servizi web, worker, bot, client di rete e script che restano attivi a lungo. Un task asincrono può essere cancellato perché l’utente chiude il programma, perché arriva un segnale di stop, oppure perché il sistema deve liberare risorse rapidamente.
In questo tutorial vedremo un sotto-argomento molto pratico e spesso trascurato: come gestire la cancellazione dei task e fare uno shutdown pulito. Imparerai a usare asyncio.Task, task.cancel(), la gestione di asyncio.CancelledError e la chiusura ordinata di risorse come connessioni, file o sessioni di rete.
Questo approccio è essenziale per scrivere codice asincrono robusto, evitando task “orfani”, risorse lasciate aperte e comportamenti imprevedibili durante l’arresto dell’applicazione.
Codice completo
import asyncio
async def fetch_data(name: str, delay: float) -> str:
"""
Simula un´operazione asincrona lunga.
Durante l´attesa può essere cancellata.
"""
print(f"[{name}] avviato")
try:
# Simuliamo lavoro I/O-bound
await asyncio.sleep(delay)
print(f"[{name}] completato")
return f"risultato-{name}"
except asyncio.CancelledError:
print(f"[{name}] cancellato: eseguo pulizia finale")
# Qui puoi chiudere connessioni, salvare stato, rilasciare risorse...
raise # Importante: rilanciare l´eccezione dopo la cleanup
async def worker(stop_event: asyncio.Event) -> None:
"""
Task principale che continua a lavorare finché non riceve il segnale di stop.
"""
i = 0
try:
while not stop_event.is_set():
i += 1
print(f"[worker] ciclo {i}")
await asyncio.sleep(0.5)
except asyncio.CancelledError:
print("[worker] cancellato: chiusura ordinata in corso")
raise
finally:
print("[worker] cleanup finale eseguito")
async def main() -> None:
stop_event = asyncio.Event()
# Creiamo task separati
t1 = asyncio.create_task(fetch_data("A", 2.0))
t2 = asyncio.create_task(fetch_data("B", 5.0))
t3 = asyncio.create_task(worker(stop_event))
try:
# Aspettiamo un po´ e poi simuliamo una richiesta di arresto
await asyncio.sleep(3)
print("[main] richiesta di shutdown ricevuta")
# Segnaliamo al worker di fermarsi
stop_event.set()
# Cancelliamo il task ancora in esecuzione
t2.cancel()
# Aspettiamo che tutti i task terminino davvero
results = await asyncio.gather(t1, t2, t3, return_exceptions=True)
print("[main] risultati finali:", results)
finally:
# In caso di errore o uscita anticipata, assicuriamoci che
# eventuali task rimasti vengano cancellati.
for task in (t1, t2, t3):
if not task.done():
task.cancel()
await asyncio.gather(t1, t2, t3, return_exceptions=True)
print("[main] shutdown pulito completato")
if __name__ == "__main__":
asyncio.run(main()) Spiegazione
Il punto chiave della programmazione asincrona non è solo avviare task, ma anche terminarli correttamente. Il codice sopra mostra tre elementi importanti:
1. Creazione dei task con asyncio.create_task()
Quando chiami asyncio.create_task(), la coroutine viene programmata per l’esecuzione concorrente. Non viene eseguita subito in modo bloccante: il loop potrà alternarla con altri task quando incontra punti di attesa come await asyncio.sleep() o operazioni di I/O.
2. Cancellazione con task.cancel()
Il metodo cancel() non interrompe il task istantaneamente come un “kill” brutale. Invia una richiesta di cancellazione. La coroutine riceverà una asyncio.CancelledError al prossimo punto di sospensione utile.
Per questo motivo è importante che il codice asincrono usi spesso await: è lì che la cancellazione può essere intercettata e gestita.
3. Gestione di asyncio.CancelledError
Nel nostro esempio, sia fetch_data() sia worker() intercettano CancelledError. Questo serve per eseguire una pulizia finale: chiudere socket, rilasciare lock, salvare dati temporanei o chiudere sessioni HTTP.
Attenzione: dopo la cleanup, l’eccezione va quasi sempre rilanciata con raise. Se la sopprimi senza motivo, il task sembrerà terminato normalmente e il codice chiamante potrebbe non capire che è stato cancellato.
4. Uso di asyncio.Event per uno stop cooperativo
Per il task worker() abbiamo usato asyncio.Event. È una soluzione elegante quando vuoi fermare un task senza cancellarlo forzatamente. Il task controlla periodicamente stop_event.is_set() e si interrompe in modo naturale. Questo è spesso preferibile alla cancellazione diretta, perché permette una logica di uscita più controllata.
5. Raccolta finale con asyncio.gather(..., return_exceptions=True)
Quando cancelli task, è buona pratica attendere che finiscano davvero. asyncio.gather(..., return_exceptions=True) impedisce che una cancellazione o un errore di un task blocchi la raccolta degli altri risultati. In un shutdown, l’obiettivo non è “fallire velocemente”, ma “chiudere bene”.
Best practice
- Gestisci sempre la cancellazione nei task che fanno I/O o loop lunghi.
- Usa
finallyper cleanup garantito: è il posto giusto per chiudere risorse. - Rilancia
CancelledErrordopo aver fatto la pulizia, salvo casi davvero specifici. - Preferisci uno stop cooperativo con
asyncio.Eventquando vuoi una terminazione più ordinata. - Evita task “fire and forget” senza una strategia di arresto: in applicazioni reali diventano difficili da controllare.
- Chiudi sempre le risorse esterne: sessioni HTTP, connessioni DB, file, stream e client di rete.
- Usa
asyncio.run()come entry point principale: semplifica la gestione del loop e dello shutdown.
Riepilogo
La cancellazione dei task è una parte fondamentale della programmazione asincrona in Python. Saper avviare coroutine è utile, ma saperle fermare correttamente è ciò che distingue un esempio didattico da un’applicazione affidabile.
In questo tutorial hai visto come:
- creare task asincroni con
asyncio.create_task(); - richiedere la cancellazione con
task.cancel(); - gestire
asyncio.CancelledError; - usare
asyncio.Eventper uno stop cooperativo; - chiudere tutto con uno shutdown pulito e prevedibile.
Se costruisci servizi asincroni, questa è una competenza indispensabile: riduce bug difficili da riprodurre e rende il codice più professionale e mantenibile.
Approfondisci con risorse ufficiali
- Documentazione ufficiale di asyncio: https://docs.python.org/3/library/asyncio.html
- asyncio.Task: https://docs.python.org/3/library/asyncio-task.html#task-object
- asyncio.CancelledError: https://docs.python.org/3/library/asyncio-exceptions.html#asyncio.CancelledError
- asyncio.Event: https://docs.python.org/3/library/asyncio-sync.html#event
- asyncio.gather(): https://docs.python.org/3/library/asyncio-task.html#asyncio.gather
