asyncio
Prima c'erano Python threading
e Python multiprocessing
... o librerie di terze parti come Twisted
e Tornado
... adesso
si utlizza asyncio
.
La libreria asyncio
è predefinita in Python sin dalla versione 3.7
!!! Se volete utilizzarla (e in molti casi... fate molto bene!!)
dovete solo aver cura di avere una versione recente di Python installata.
Documentazione ufficiale
La documentazione ufficiale di asyncio
è disponibile nella documentazione ufficiale di Python. Link veloce per i più pigri:
Clicca qui!.
Questo piccolo tutorial serve solo come introduzione al discorso...
asyncio
è un framework asincrono di I/O che permette di scrivere codice concorrente tramite la sintassi await
e async
.
Si basa sui concetti di event loop e di coroutines per la gestione delle operazioni asincrone.
Vediamoli:
Event Loop
L'event loop (letteralmente, il ciclo degli eventi) è il concetto alla base del modello di esecuzione di asyncio. Quando viene attivato, esso si occupa di pianificare e gestire le operazioni di I/O e le attività (asincrone) definite nelle coroutines e di alternarsi con il sistema operativo nel controllo dell'hardware: in questo modo si mantiene il sistema fluido (cioè in esecuzione di operazioni non bloccanti).
Coroutines
Le coroutine(s) non sono altro che funzioni asincrone, ovvero definite con la sintassi async def
. La loro caratteristica principale, rispetto
alle normali funzioni è che hanno la facoltà di restituire il controllo all'event loop tramite l'utilizzo della keyword await
, permettendo ad altre
operazioni di essere eseguite nel frattempo.
import asyncio
async def saluta(nome, delay):
""" Aspetta {delay} secondi (asincrono) e poi saluta {nome}"""
await asyncio.sleep(delay)
print(f"Ciao, {nome}!")
async def main():
""" esegue TOT operazioni 'contemporaneamente'!!! """
# asyncio.gather permette di eseguire 'insieme' un gruppo di operazioni asincrone
await asyncio.gather(
saluta("Andrea", 2),
saluta("Barbara", 1),
saluta("Cristina", 3),
)
if __name__ == "__main__":
# codice per avviare l'event loop
asyncio.run(main())
Task(s)
Un task asyncio è una coroutine in programma per l'esecuzione e gestita in maniera indipendente.
import asyncio
async def operazioneLunga(n):
print(f"Inizio operazione {n}")
await asyncio.sleep(n)
print(f"Fine operazione {n}")
async def main():
# asyncio.create_task... fa quello che dice!
task1 = asyncio.create_task(operazioneLunga(2))
task2 = asyncio.create_task(operazioneLunga(4))
# aspetta il completamento dei task
await task1
await task2
if __name__ == "__main__":
asyncio.run(main())
Gestione errori e Timeout
Con tutta la buona volontà di portare avanti e terminare più operazioni possibili contemporaneamente, è praticamente certo che prima o poi qualcosa andrà storto... bisogna rendere il codice più robusto possibile, ovvero in grado di gestire al meglio ogni eventuale problematica incontrata.
Le soluzioni offerte da Python sono tipicamente due:
- la gestione delle eccezioni tramite il codice
try... except
: questa vale sempre!!! - l'impostazione di un timeout per le coroutine in esecuzione (oltre il quale non possono andare): questa vale solo per asyncio!!!
Per gestire ogni problema al meglio bisognerebbe considerare entrambi gli approcci. Vediamo un esempio di codice semplice e ben commentato!
import asyncio
async def fai_in_fretta():
""" aspetta 5 secondi e via! """
try:
await asyncio.sleep(5)
print("Fatto!")
except asyncio.CancelledError:
print("Operazione cancellata!!!")
async def main():
task = asyncio.create_task(fai_in_fretta())
try:
# concede al massimo 2 secondi di tempo per l'esecuzione del task
await asyncio.wait_for(task, timeout=2)
except asyncio.TimeoutError:
print("Time OUT")
task.cancel()
await task
if __name__ == "__main__":
asyncio.run(main())
Asyncio Networking
La libreria asyncio
rende molto semplice lavorare con le operazioni I/O di rete. L'esempio sotto presenta un echo server
(una applicazione che ripete tutto quello che dici) che utilizza le classi asyncio StreamReader
e StreamWriter
import asyncio
async def echo(reader, writer):
while True:
data = await reader.read(100)
if not data:
break
writer.write(data)
await writer.drain()
writer.close()
await writer.wait_closed()
async def main():
server = await asyncio.start_server(echo, "127.0.0.1", 8888)
async with server:
await server.serve_forever()
if __name__ == "__main__":
asyncio.run(main())
Debugging
Per fare il debug di una applicazione basata su asyncio, ti basta inserire la riga
asyncio.set_debug(True)
prima di eseguire l'event loop.
Fornirà all'applicazione la possibilità di essere controllata al meglio!!!
3rd party libraries
Vista la grande qualità per cui asyncio si è subita distinta, sono immediatamente state implementate librerie specifiche, basate su di essa per i casi più disparati:
aiohttp
, una libreria che implementa framework HTTP client/serveraiomysql
, una libreria asincrona per interagire con il database MySQL e MariaDBAsyncSSH
, una libreria asincorna per implementare un client o un server SSHaiodns
, un semplice DNS resolver basato su asyncioaioping
, una libreria asincrona per il protocollo ICMP (su cui si basa ping)asynctest
, una libreria per i test su funzioni e librerie asincrone-
Molti (ma non tutti) sono genericamente indicati su questo strano sito: Awesome AsyncIO. Trovare esempi in rete di utilizzo di queste librerie diventa ogni giorno più facile.
Best Practices
- Usa
async def
per definire una coroutine eawait
per metterla in pausa. - Usa
asyncio.gather()
oasyncio.create_task()
per eseguire coroutine in maniera concorrente. - Gestisci le eccezioni nei blocchi
try... except
. - Usa
asyncio.wait_for()
per impostare timeout alle coroutines. - Chiudi sempre le risorse, come i file o le connessioni di rete, quando non le usi più.
- Usa librerie di terze parti basate su asyncio per le migliori performance su compiti specifici.
- Usa il debug mode di asyncio per controllare il codice.