Cursus
De module asyncio werd in Python 3.4 toegevoegd als een voorlopige package. Dat betekent dat het mogelijk is dat asyncio achterwaarts onverenigbare wijzigingen krijgt of zelfs verwijderd kan worden in een toekomstige release van Python.
Volgens de documentatie “biedt asyncio infrastructuur voor het schrijven van single-threaded gelijktijdige code met coroutines, het multiplexen van I/O-toegang over sockets en andere resources, het draaien van netwerkclients en -servers en andere gerelateerde primitieven“. Dit hoofdstuk is niet bedoeld om alles te behandelen wat je met asyncio kunt doen, maar je leert wel hoe je de module gebruikt en waarom die nuttig is.
Als je iets als asyncio nodig hebt in een oudere versie van Python, kijk dan eens naar Twisted of gevent.
Definities
De module asyncio biedt een framework dat draait om de eventloop. Een eventloop wacht in feite tot er iets gebeurt en reageert dan op de gebeurtenis. Hij is verantwoordelijk voor het afhandelen van zaken als I/O en systeemevents.
asyncio heeft eigenlijk meerdere implementaties van de loop beschikbaar. De module kiest standaard degene die waarschijnlijk het efficiëntst is voor het besturingssysteem waarop hij draait; je kunt de eventloop echter ook expliciet kiezen als je dat wilt. Een eventloop zegt in feite: “als event A gebeurt, reageer dan met functie B”.
Denk aan een server die wacht tot iemand langskomt en om een resource vraagt, zoals een webpagina. Als de website niet erg populair is, is de server lang inactief. Maar wanneer er wel een hit binnenkomt, moet de server reageren. Deze reactie staat bekend als eventafhandeling. Wanneer een gebruiker de webpagina laadt, zal de server één of meer eventhandlers controleren en aanroepen. Zodra die eventhandlers klaar zijn, moeten ze de controle teruggeven aan de eventloop. In Python gebruikt asyncio hiervoor coroutines.
Een coroutine is een speciale functie die de controle aan de aanroeper kan teruggeven zonder zijn staat te verliezen. Een coroutine is een consument én een uitbreiding van een generator. Een van hun grote voordelen ten opzichte van threads is dat ze heel weinig geheugen gebruiken om uit te voeren. Let op: wanneer je een coroutine-functie aanroept, wordt die niet daadwerkelijk uitgevoerd. In plaats daarvan retourneert hij een coroutine-object dat je aan de eventloop kunt doorgeven om het direct of later uit te voeren.
Een andere term die je waarschijnlijk tegenkomt bij het gebruik van de module asyncio is future. Een future is eigenlijk een object dat het resultaat vertegenwoordigt van werk dat nog niet is voltooid. Je eventloop kan future-objecten in de gaten houden en wachten tot ze klaar zijn. Wanneer een future klaar is, staat hij op done. Asyncio ondersteunt ook locks en semaforen.
Het laatste onderdeel dat ik wil noemen is de Task. Een Task is een wrapper voor een coroutine en een subklasse van Future. Je kunt een Task zelfs inplannen met de eventloop.
async and await
De sleutelwoorden async en await zijn toegevoegd in Python 3.5 om een native coroutine te definiëren en die te onderscheiden van een coroutine op basis van een generator. Als je een diepgaande beschrijving van async en await wilt, bekijk dan PEP 492.
In Python 3.4 maak je een coroutine als volgt:
Deze decorator werkt nog steeds in Python 3.5, maar de module types kreeg een update in de vorm van een coroutine-functie die je nu kan vertellen of je met een native coroutine te maken hebt of niet. Vanaf Python 3.5 kun je async def gebruiken om syntactisch een coroutine-functie te definiëren.
De functie hierboven zou er dan zo uitzien:
Wanneer je op deze manier een coroutine definieert, kun je geen yield gebruiken binnen de coroutine-functie. In plaats daarvan moet die een return- of await-statement bevatten die worden gebruikt om waarden aan de aanroeper terug te geven. Let op: het sleutelwoord await kan alleen worden gebruikt binnen een async def-functie.
De sleutelwoorden async/await kun je zien als een API voor asynchroon programmeren. De asyncio-module is slechts een framework dat toevallig async/await gebruikt om asynchroon te programmeren. Er bestaat zelfs een project genaamd curio dat dit concept aantoont, omdat het een aparte implementatie is van een eventloop die onder water async/await gebruikt.
Een slecht coroutine-voorbeeld
Hoewel het zeker helpt om veel achtergrondinformatie te hebben over hoe dit allemaal werkt, wil je soms gewoon voorbeelden zien zodat je gevoel krijgt voor de syntax en hoe je dingen in elkaar zet.
Met dat in gedachten beginnen we met een simpel voorbeeld!
Een vrij veelvoorkomende taak is het downloaden van een bestand van ergens, of dat nu een interne resource is of een bestand op internet. Meestal wil je meer dan één bestand downloaden.
Laten we daarom een paar coroutines maken die dat kunnen doen:
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 deze code importeren we de modules die we nodig hebben en maken we vervolgens onze eerste coroutine met de async-syntaxis. Deze coroutine heet download_coroutine en gebruikt Python’s urllib om elke URL te downloaden die eraan wordt doorgegeven. Als hij klaar is, retourneert hij een bericht dat dat aangeeft.
De andere coroutine is onze main-coroutine. Die neemt in feite een lijst met één of meer URL’s en zet die in de wachtrij. We gebruiken wait van asyncio om te wachten tot de coroutines klaar zijn. Maar om de coroutines daadwerkelijk te starten, moeten ze aan de eventloop worden toegevoegd. Dat doen we helemaal aan het eind, waar we een eventloop ophalen en vervolgens de methode run_until_complete aanroepen. Je ziet dat we de main-coroutine aan de eventloop doorgeven. Dit start de main-coroutine, die de tweede coroutine in de wachtrij zet en laat lopen. Dit staat bekend als een chained coroutine.
Het probleem met dit voorbeeld is dat het eigenlijk helemaal geen coroutine is. De reden is dat de functie download_coroutine niet asynchroon is. Het probleem hier is dat urllib niet asynchroon is en bovendien gebruik ik noch await noch yield. Een betere manier om dit te doen is met het pakket aiohttp. Laten we dat daarna bekijken!
Een beter coroutine-voorbeeld
Het pakket aiohttp is ontworpen voor het maken van asynchrone HTTP-clients en -servers. Je kunt het met pip installeren als volgt:
pip install aiohttp
Zodra dat geïnstalleerd is, werken we onze code bij om aiohttp te gebruiken zodat we de bestanden kunnen downloaden:
Je ziet hier dat we een paar nieuwe items importeren: aiohttp en async_timeout. Die laatste is eigenlijk een van de afhankelijkheden van aiohttp en laat ons een timeout-contextmanager maken. Laten we onderaan de code beginnen en naar boven werken. In de onderste conditionele instructie starten we onze asynchrone eventloop en roepen we onze main-functie aan.
In de main-functie maken we een ClientSession-object dat we doorgeven aan onze download-coroutinefunctie voor elk van de urls die we willen downloaden. In de download_coroutine maken we een async_timeout.timeout()-contextmanager die in feite een timer van X seconden maakt. Wanneer de seconden op zijn, eindigt de contextmanager of time-out hij. In dit geval is de timeout 10 seconden. Vervolgens roepen we de get()-methode van onze sessie aan, die ons een response-object geeft. Nu komen we bij het deel dat een beetje magisch is. Wanneer je het content-attribuut van het response-object gebruikt, krijg je een instantie van aiohttp.StreamReader, waarmee we het bestand in brokken van willekeurige grootte kunnen downloaden. Terwijl we het bestand lezen, schrijven we het naar de lokale schijf. Tot slot roepen we de methode release() van de response aan, die de verwerking van de response afrondt.
Volgens de documentatie van aiohttp wordt, omdat het response-object in een contextmanager is gemaakt, release() technisch gezien impliciet aangeroepen. Maar in Python is expliciet meestal beter en er staat een opmerking in de documentatie dat we er niet op moeten vertrouwen dat de verbinding vanzelf verdwijnt, dus ik vind het in dit geval beter om die expliciet vrij te geven.
Er is hier nog één deel dat blokkerend is, en dat is het deel van de code dat daadwerkelijk naar schijf schrijft. Terwijl we het bestand wegschrijven, blokkeren we nog steeds. Er is een andere bibliotheek genaamd aiofiles die we zouden kunnen gebruiken om ook het wegschrijven van bestanden asynchroon te maken, maar die update laat ik aan de lezer over.
Afspraken inplannen
Je kunt ook aanroepen naar gewone functies inplannen met de asyncio-eventloop. De eerste methode die we bekijken is call_soon. De methode call_soon roept je callback of eventhandler in principe zo snel mogelijk aan. Hij werkt als een FIFO-queue, dus als sommige callbacks wat langer duren, worden de andere pas uitgevoerd nadat de vorige klaar zijn.
Laten we naar een voorbeeld kijken:
De meeste functies van asyncio accepteren geen keyword-argumenten, dus hebben we de module functools nodig als we keywords aan onze eventhandler willen doorgeven. Onze gewone functie print wat tekst naar stdout wanneer die wordt aangeroepen. Als je het argument stop op True zet, stopt hij ook de eventloop.
De eerste keer dat we hem aanroepen, stoppen we de loop niet. De tweede keer dat we hem aanroepen, stoppen we de loop wel. De reden dat we de loop willen stoppen, is dat we hem run_forever hebben gegeven, wat de eventloop in een oneindige lus zet. Zodra de loop is gestopt, kunnen we hem sluiten.
Als je deze code uitvoert, zou je de volgende output moeten zien:
starting event loop
Event handler called
Event handler called
stopping the loop
closing event loop
Er is een gerelateerde functie genaamd call_soon_threadsafe. Zoals de naam al aangeeft, werkt die hetzelfde als call_soon, maar dan thread-safe. Als je een aanroep daadwerkelijk wilt uitstellen tot een moment in de toekomst, kun je dat doen met de functie call_later. In dit geval kunnen we onze call_soon-aanroep aanpassen naar het volgende:
loop.call_later(1, event_handler, loop)
Dit stelt het aanroepen van onze eventhandler één seconde uit, daarna wordt hij aangeroepen en wordt de loop als eerste parameter meegegeven. Als je een specifiek tijdstip in de toekomst wilt inplannen, moet je de tijd van de loop pakken in plaats van de systeemtijd. Dat kan zo:
current_time = loop.time()
Als je die hebt, kun je de functie call_at gebruiken en hem het tijdstip geven waarop je je eventhandler wilt laten aanroepen. Stel dat we onze eventhandler over vijf minuten willen aanroepen. Zo kun je dat doen:
loop.call_at(current_time + 300, event_handler, loop)
In dit voorbeeld gebruiken we de huidige tijd die we hebben opgehaald en tellen we daar 300 seconden, oftewel vijf minuten, bij op. Zo stellen we het aanroepen van onze eventhandler vijf minuten uit! Best handig!
Tasks
Tasks zijn een subklasse van een Future en een wrapper om een coroutine heen. Ze geven je de mogelijkheid om bij te houden wanneer ze klaar zijn met verwerken. Omdat ze een type Future zijn, kunnen andere coroutines op een task wachten en kun je ook het resultaat van een task ophalen wanneer die klaar is.
Laten we een eenvoudig voorbeeld bekijken:
Hier maken we een asynchrone functie die het aantal seconden aanneemt dat de functie nodig heeft om te draaien. Dit simuleert een langlopende taak. Vervolgens maken we onze eventloop en daarna een task-object door de functie create_task van het eventloop-object aan te roepen. De functie create_task accepteert de functie die we in een task willen omzetten. Vervolgens vertellen we de eventloop om te draaien totdat de task voltooid is. Helemaal aan het eind halen we het resultaat van de task op, omdat die klaar is.
Tasks kunnen ook heel eenvoudig worden geannuleerd met hun methode cancel. Roep die gewoon aan wanneer je een task wilt beëindigen. Als een task wordt geannuleerd terwijl hij op een andere bewerking wacht, zal de task een CancelledError opwerpen.
Afronding
Op dit punt zou je genoeg moeten weten om zelf met de asyncio-bibliotheek aan de slag te gaan. De asyncio-bibliotheek is erg krachtig en laat je heel veel coole en interessante taken uitvoeren. De asyncio-bibliotheek is ontworpen voor netwerksockets.
Een geweldige bibliotheek die asynchroon socketprogrammeren mogelijk maakte vóór asyncio is het Twisted-framework. Een ander interessant project is Dask, een flexibele parallelle computing-bibliotheek voor analytisch rekenen. Duik gewoon in de documentatie van de module asyncio en laat je ideeën stromen!
