Corso
Il modulo asyncio è stato aggiunto a Python nella versione 3.4 come pacchetto provvisorio. Questo significa che è possibile che asyncio subisca modifiche incompatibili con le versioni precedenti o possa persino essere rimosso in una futura release di Python.
Secondo la documentazione, asyncio “fornisce l'infrastruttura per scrivere codice concorrente monothread usando coroutine, multiplexare l'accesso I/O su socket e altre risorse, eseguire client e server di rete e altre primitive correlate“. Questo capitolo non intende coprire tutto ciò che puoi fare con asyncio, ma imparerai a usare il modulo e perché è utile.
Se ti serve qualcosa come asyncio in una versione precedente di Python, potresti dare un'occhiata a Twisted o gevent.
Definizioni
Il modulo asyncio fornisce un framework che ruota attorno al ciclo degli eventi (event loop). Un event loop fondamentalmente aspetta che succeda qualcosa e poi reagisce all'evento. È responsabile della gestione di elementi come I/O ed eventi di sistema.
asyncio dispone in realtà di diverse implementazioni del loop. Il modulo utilizzerà come predefinita quella che probabilmente è la più efficiente per il sistema operativo su cui gira; tuttavia puoi scegliere esplicitamente l'event loop se lo desideri. Un event loop, in sostanza, dice “quando l'evento A accade, reagisci con la funzione B”.
Pensa a un server mentre aspetta che qualcuno arrivi e chieda una risorsa, come una pagina web. Se il sito non è molto popolare, il server resterà inattivo a lungo. Ma quando arriva una richiesta, allora il server deve reagire. Questa reazione è nota come gestione degli eventi. Quando un utente carica la pagina, il server verifica e chiama uno o più event handler. Una volta che questi handler hanno finito, devono restituire il controllo all'event loop. Per farlo in Python, asyncio usa le coroutine.
Una coroutine è una funzione speciale che può cedere il controllo al suo chiamante senza perdere il proprio stato. Una coroutine è una consumatrice e un'estensione di un generatore. Uno dei loro grandi vantaggi rispetto ai thread è che non usano molta memoria per l'esecuzione. Nota che quando chiami una funzione coroutine, in realtà non viene eseguita. Invece restituisce un oggetto coroutine che puoi passare all'event loop per farlo eseguire immediatamente o in seguito.
Un altro termine che probabilmente incontrerai usando il modulo asyncio è future. Un future è in pratica un oggetto che rappresenta il risultato di un lavoro che non è ancora completato. Il tuo event loop può osservare gli oggetti future e aspettare che finiscano. Quando un future termina, viene impostato su done. Asyncio supporta anche lock e semafori.
L'ultimo elemento che voglio menzionare è il Task. Un Task è un wrapper per una coroutine e una sottoclasse di Future. Puoi anche pianificare (schedulare) un Task usando l'event loop.
async e await
Le keyword async e await sono state aggiunte in Python 3.5 per definire coroutine native e renderle un tipo distinto rispetto alle coroutine basate su generatori. Se vuoi una descrizione approfondita di async e await, dai un'occhiata a PEP 492.
In Python 3.4, creeresti una coroutine così:
Questo decorator funziona ancora in Python 3.5, ma il modulo types ha ricevuto un aggiornamento sotto forma di una funzione coroutine che ora ti dirà se ciò con cui stai interagendo è una coroutine nativa oppure no. A partire da Python 3.5, puoi usare async def per definire sintatticamente una funzione coroutine.
Quindi la funzione qui sopra finirebbe per assomigliare a questa:
Quando definisci una coroutine in questo modo, non puoi usare yield all'interno della funzione coroutine. Deve invece includere un'istruzione return o await che viene usata per restituire valori al chiamante. Nota che la keyword await può essere usata solo dentro una funzione async def.
Le keyword async/await possono essere considerate un'API da usare per la programmazione asincrona. Il modulo asyncio è solo un framework che per caso usa async/await per programmare in modo asincrono. Esiste in realtà un progetto chiamato curio che dimostra questo concetto, in quanto è un'implementazione separata di un event loop che usa async/await sotto il cofano.
Un cattivo esempio di coroutine
Per quanto sia certamente utile avere molte informazioni di base su come funziona tutto questo, a volte vuoi solo vedere qualche esempio per farti un'idea della sintassi e di come mettere insieme le cose.
Con questo in mente, iniziamo con un esempio semplice!
Un'attività piuttosto comune che vorrai completare è scaricare un file da qualche posizione, che si tratti di una risorsa interna o di un file su Internet. Di solito vorrai scaricare più di un file.
Quindi creiamo una coppia di coroutine che possano farlo:
import asyncio
import os
import urllib.request
async def download_coroutine(url):
#"A coroutine to download the specified url"
request = urllib.request.urlopen(url)
filename = os.path.basename(url)
with open(filename, 'wb') as file_handle:
while True:
chunk = request.read(1024)
if not chunk:
break
file_handle.write(chunk)
msg = 'Finished downloading {filename}'.format(filename=filename)
return msg
async def main(urls):
"""
Creates a group of coroutines and waits for them to finish
"""
coroutines = [download_coroutine(url) for url in urls]
completed, pending = await asyncio.wait(coroutines)
for item in completed:
print(item.result())
if __name__ == '__main__':
urls = ["http://www.irs.gov/pub/irs-pdf/f1040.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
"http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"]
event_loop = asyncio.get_event_loop()
try:
event_loop.run_until_complete(main(urls))
finally:
event_loop.close()
In questo codice importiamo i moduli necessari e poi creiamo la nostra prima coroutine usando la sintassi async. Questa coroutine si chiama download_coroutine e usa urllib di Python per scaricare qualunque URL le venga passato. Quando ha finito, restituirà un messaggio che lo indica.
L'altra coroutine è la nostra coroutine principale. Prende in pratica un elenco di uno o più URL e li mette in coda. Usiamo la funzione wait di asyncio per aspettare che le coroutine finiscano. Ovviamente, per avviare effettivamente le coroutine, devono essere aggiunte all'event loop. Lo facciamo proprio alla fine, dove otteniamo un event loop e poi chiamiamo il suo metodo run_until_complete. Noterai che passiamo la coroutine principale all'event loop. Questo avvia l'esecuzione della coroutine principale che mette in coda la seconda coroutine e la fa partire. Questo è noto come coroutine concatenata (chained coroutine).
Il problema di questo esempio è che in realtà non è affatto una coroutine. Il motivo è che la funzione download_coroutine non è asincrona. Il problema qui è che urllib non è asincrono e inoltre non sto usando await o yield. Un modo migliore per farlo sarebbe usare il pacchetto aiohttp. Vediamolo subito!
Un esempio migliore di coroutine
Il pacchetto aiohttp è pensato per creare client e server HTTP asincroni. Puoi installarlo con pip in questo modo:
pip install aiohttp
Una volta installato, aggiorniamo il nostro codice per usare aiohttp in modo da poter scaricare i file:
Noterai qui che importiamo un paio di nuovi elementi: aiohttp e async_timeout. Quest'ultimo è in realtà una delle dipendenze di aiohttp e ci permette di creare un context manager di timeout. Partiamo dal fondo del codice e risaliamo. Nell'istruzione condizionale finale, avviamo il nostro event loop asincrono e chiamiamo la funzione principale.
Nella funzione principale, creiamo un oggetto ClientSession che passiamo alla nostra funzione coroutine di download per ognuno degli URL che vogliamo scaricare. In download_coroutine, creiamo un context manager async_timeout.timeout() che di fatto crea un timer di X secondi. Quando i secondi scadono, il context manager termina o va in timeout. In questo caso, il timeout è di 10 secondi. Poi chiamiamo il metodo get() della sessione che ci restituisce un oggetto response. Ora arriviamo alla parte un po' magica. Quando usi l'attributo content dell'oggetto response, ottieni un'istanza di aiohttp.StreamReader che ci permette di scaricare il file a chunk della dimensione che preferiamo. Man mano che leggiamo il file, lo scriviamo su disco locale. Infine chiamiamo il metodo release() della response, che terminerà l'elaborazione della risposta.
Secondo la documentazione di aiohttp, poiché l'oggetto response è stato creato in un context manager, tecnicamente chiama release() in modo implicito. Ma in Python, l'esplicito è solitamente migliore e c'è una nota nella documentazione che sconsiglia di fare affidamento sulla semplice chiusura della connessione, quindi ritengo che in questo caso sia meglio rilasciarla esplicitamente.
C'è una parte che è ancora bloccante, ovvero la porzione di codice che effettua la scrittura su disco. Mentre scriviamo il file, stiamo comunque bloccando. Esiste un'altra libreria chiamata aiofiles che potremmo usare per provare a rendere asincrona anche la scrittura del file, ma lascerò questo aggiornamento al lettore.
Pianificare le chiamate
Puoi anche pianificare chiamate a funzioni normali usando l'event loop di asyncio. Il primo metodo che vedremo è call_soon. Il metodo call_soon chiamerà sostanzialmente il tuo callback o event handler appena possibile. Funziona come una coda FIFO, quindi se alcune callback impiegano un po' a essere eseguite, le successive verranno ritardate finché le precedenti non avranno terminato.
Vediamo un esempio:
La maggior parte delle funzioni di asyncio non accetta keyword argument, quindi avremo bisogno del modulo functools se dobbiamo passare keyword al nostro event handler. La nostra funzione normale stamperà un po' di testo su stdout ogni volta che viene chiamata. Se imposti il suo argomento stop su True, fermerà anche l'event loop.
La prima volta che la chiamiamo, non fermiamo il loop. La seconda volta che la chiamiamo, invece, fermiamo il loop. Il motivo per cui vogliamo fermare il loop è che gli abbiamo detto di run_forever, che metterà l'event loop in un ciclo infinito. Una volta fermato il loop, possiamo chiuderlo.
Se esegui questo codice, dovresti vedere il seguente output:
starting event loop
Event handler called
Event handler called
stopping the loop
closing event loop
C'è una funzione correlata chiamata call_soon_threadsafe. Come suggerisce il nome, funziona allo stesso modo di call_soon, ma è thread-safe. Se vuoi effettivamente ritardare una chiamata a un momento futuro, puoi farlo usando la funzione call_later. In questo caso, potremmo cambiare la nostra firma di call_soon nella seguente:
loop.call_later(1, event_handler, loop)
Questo ritarderà di un secondo la chiamata del nostro event handler, poi lo chiamerà e passerà il loop come primo parametro. Se vuoi programmare un momento specifico nel futuro, allora dovrai prendere l'ora del loop invece dell'orologio del computer. Puoi farlo così:
current_time = loop.time()
Una volta che ce l'hai, puoi semplicemente usare la funzione call_at e passarle l'orario in cui vuoi che chiami il tuo event handler. Diciamo che vogliamo chiamare il nostro event handler tra cinque minuti. Ecco come potresti farlo:
loop.call_at(current_time + 300, event_handler, loop)
In questo esempio, usiamo l'ora corrente che abbiamo preso e aggiungiamo 300 secondi, ovvero cinque minuti. In questo modo, ritardiamo la chiamata del nostro event handler di cinque minuti! Niente male!
Task
I Task sono una sottoclasse di Future e un wrapper attorno a una coroutine. Ti danno la possibilità di tenere traccia di quando terminano l'elaborazione. Poiché sono un tipo di Future, altre coroutine possono aspettare un task e puoi anche recuperare il risultato di un task quando ha finito di elaborare.
Diamo un'occhiata a un semplice esempio:
Qui creiamo una funzione asincrona che accetta il numero di secondi che serviranno alla funzione per essere eseguita. Questo simula un processo di lunga durata. Poi creiamo il nostro event loop e quindi creiamo un oggetto task chiamando la funzione create_task dell'oggetto event loop. La funzione create_task accetta la funzione che vogliamo trasformare in un task. Poi diciamo all'event loop di eseguire fino a quando il task non completa. Alla fine, otteniamo il risultato del task dato che è terminato.
I Task possono anche essere annullati molto facilmente usando il loro metodo cancel. Chiamalo quando vuoi terminare un task. Se un task viene annullato mentre è in attesa di un'altra operazione, il task solleverà un CancelledError.
Conclusioni
A questo punto dovresti sapere abbastanza per iniziare a lavorare da solo con la libreria asyncio. La libreria asyncio è molto potente e ti permette di fare un sacco di attività davvero interessanti. La libreria asyncio è stata progettata per i socket di rete.
Un'ottima libreria che consente la programmazione di socket asincroni prima di asyncio è il framework Twisted. Un altro progetto interessante è Dask, una libreria flessibile di calcolo parallelo per l'analisi. Dai un'occhiata alla documentazione del modulo asyncio e lascia fluire le idee!

