Asynchronous programming in Python

Asynchronous programming is een relatief nieuwe feature in Python, een bij Antagonist veelgebruikte programmeertaal. Je benut daarmee op een makkelijke manier de processortijd zo efficiënt mogelijk. Graag vertel ik je daar meer over, want dit kwam recentelijk flink van pas! Lees wat je als developer aan deze feature hebt en hoe je het gebruikt.

Asynchronous programming met Python

Onlangs wilden we indexeren hoeveel van de hostingpakketten die bij ons draaien nu daadwerkelijk in actief gebruik zijn. Om dit te meten, was het idee om te kijken hoeveel websites nog de standaard placeholder van Antagonist toonden. Een scriptje om dit te testen, was natuurlijk zo geschreven. Er was alleen één probleem. De naïeve implementatie zou er uren over hebben gedaan om alle 100.000+ websites te checken… Dat moet sneller kunnen!

Waarom is synchroon langzaam?

Er zijn een aantal roundtrips nodig naar de webserver om een website op te vragen. Je maakt eerst een verbinding, dan verstuur je een HTTP request en pas daarna komt het antwoord met de inhoud van de pagina terug. Haal je drie webpagina’s achter elkaar op, dan ziet dat er qua CPU-gebruik ongeveer zo uit als op de afbeelding hieronder.

Waarom het synchroon opvragen niet efficiënt is.Het groene gedeelte representeert de tijd dat Python daadwerkelijk bezig is met het maken van de request en het verwerken van de data die terugkomt. De tijd daartussen, het rode gedeelte, is de tijd dat er gewacht wordt tot er antwoord is van de webserver. Zoals je ziet, is het na elkaar opvragen van de pagina’s niet erg efficiënt. Je wacht telkens op antwoord, wat ervoor zorgt dat het controleren van een website ongeveer 100 milliseconden duurt.

Dit probleem komt vaker voor in programma’s en wordt I/O bound genoemd. Dat betekent dat de snelheid van een programma gelimiteerd is door hoe snel de input en output (I/O) is. Het inlezen en wegschrijven van data, is iets wat bijna altijd een centrale rol speelt in een applicatie. Denk aan communicatie met de database voor je webapplicaties of het inladen van een programma in je RAM op je desktop.


Werken bij Antagonist

Adem jij code? Ben jij Linux-fanaat met interesse in development of droom je van GitLab pipelines en StackStorm workflows? Misschien zoeken we jou!

Bekijk vacatures →


Asynchronous programming als oplossing

Het probleem is duidelijk, maar hoe lossen we het op? Je voelt hem misschien al wel aankomen. Terwijl we wachten, kunnen we natuurlijk alvast beginnen met het opvragen van de volgende webpagina. Er zijn verschillende manieren, elk met zijn eigen voor- en nadelen. Hier kiezen we er er in ieder geval voor om van asynchronous programming gebruik te maken.

Misschien ben je zelf al asynchroon bezig, zonder dat jij je er bewust van bent. Vergelijk het met als je in de keuken staat en een recept volgt. Je maakt de pastasaus, terwijl de spaghetti ondertussen staat te koken. Eerst wachten tot spaghetti klaar is met koken en pas dan de saus maken, is niet efficiënt.

Meertaligheid
De volgende voorbeelden zijn voor Python, omdat wij dat veel gebruiken. Asynchronous programming bestaat echter in veel programmeertalen en werkt doorgaans op een vergelijkbare manier.

Asyncio in Python

Python heeft sinds versie 3.4 een nieuwe library, namelijk Ascynio. Dit is een library voor asynchrone in- en output. In Python 3.5 zijn er ook speciale keywords bijgekomen, waaronder async en await. Dit kunnen we gebruiken om asynchrone functies te bouwen. Dit ziet er bijvoorbeeld als onderstaand uit.

import asyncio

async def example():
	await asyncio.sleep(1)
	print('Hello, world!')

Deze code definieert een asynchrone functie, ook wel een coroutine genoemd. De example-functie wacht een seconde en output dan de string ‘Hello, world!’. Het cruciale verschil met een normale functie is dat we hier het keyword await kunnen gebruiken. Dit geeft aan dat we willen wachten tot de functie asyncio.sleep(1) klaar is. In de tussentijd kunnen we mooi andere dingen doen!

De asyncio.sleep-functie is namelijk ook een coroutine. Een zogenaamde event loop bepaalt dan welke coroutines worden uitgevoerd. Zodra een coroutine een andere coroutine met het keyword await aanroept, dan wordt die aan de event loop toegevoegd. Ook geeft dit keyword aan dat de functie onderbroken mag worden en dat de event loop iets anders processortijd kan geven.

Wat is een event loop?

Coroutines en event loops, dat gaat allemaal wel heel snel ineens! Misschien wordt het duidelijker met een stap-voor-stap-uitleg.

  1. De event loop is een soort lijst van gebeurtenissen of taken.
  2. Deze taken zijn de functies die asynchroon moeten worden uitgevoerd.
  3. De event loop begint met het uitvoeren van de eerste functie, in dit geval de example-functie van hierboven.
  4. Roept de example-functie de volgende coroutine met het keyword await aan, dan pauzeert de example-functie.
  5. De nieuwe coroutine (asyncio.sleep) wordt dan aan de event loop toegevoegd.
  6. De event loop gaat ten slotte verder met de volgende taak die op de lijst staat.

In dit geval staat er alleen de functie sleep op. Dit is een speciale I/O-functie die noteert dat er een seconde moet worden gewacht, voordat de functie verder mag. In de tussentijd kan de event loop door met andere coroutines. Na het wachten is de example-functie weer beschikbaar en kan de event loop verder.

Een uitgebreider voorbeeld

Nu we weten hoe coroutines en de event loop werken, kunnen we kijken naar hoe we ons probleem kunnen oplossen door middel van asynchrone functies. Hier laat ik de daadwerkelijke code voor het checken van de placeholders en het ophalen van de websites even weg. Zo blijft het overzichtelijk en focussen we ons vooral op het asynchrone gedeelte.

import asyncio, random

loop = asyncio.get_event_loop()

async def fetch_url(url):
    print(f'Bezig met opvragen van {url}')
    timeout = random.random()
    await asyncio.sleep(timeout)
    return f'Klaar met opvragen van {url} in {timeout} seconde!'

async def check_websites():
    jobs = []
    for i in range(0,10000):
        jobs.append(loop.create_task(fetch_url(f'website {i}')))
    for job in jobs:
        result = await job
        # Doe iets met het resultaat
        print(result)

loop.run_until_complete(check_websites())
loop.close()

Hierboven zie je twee functies gedefineerd: fetch_url en check_websites. Daarna wordt check_websites als eerste functie aan de event loop toegevoegd. Voer je deze functie uit, dan worden er nieuwe tasks in de event loop aangemaakt die de verschillende websites ophaalt. Daarna itereren we over de jobs-lijst heen en awaiten we elke task die we hebben toegevoegd.

Dit betekent dat de event loop hier de check_websites-functie mag pauzeren en pas verder kan gaan als de desbetreffende job klaar is met uitvoeren. De event loop gaat zo zeer waarschijnlijk met die job bezig. De fetch_url-functie wacht een willekeurige tijd (maximaal een seconde) om de vertraging van het opvragen van een website te simuleren. Deze timeout wordt op dezelfde manier afgewacht en dus kan de event loop alvast bezig met de volgende taak.

Efficiënter met asynchroon

Als we deze code asynchroon uitvoeren, dan ziet dat er ongeveer zo uit als op de onderstaande afbeelding. Het CPU-gebruik is daar een stuk efficiënter! Eerst worden aan één stuk alle pagina’s opgevraagd. Het wacht daarna tot de eerste resultaten binnen zijn en verwerkt die. Tijdens het verwerken, komen de rest van de resultaten binnen.

Asynchronous programming met Python.

De verhouding tussen het wachten en hoe lang alles opvragen duurt, hangt af van de hoeveelheid pagina’s. Moeten er meer worden opgevraagd, dan zijn de eerste resultaten al binnen als de event loop klaar is met het opvragen. Het kan dan dus meteen verder met het verwerken van de resultaten. De totale duur is afhankelijk van de traagste pagina. Je kunt dus eventueel nog op een handjevol pagina’s moeten wachten, nadat de resultaten zijn verwerkt.

Tot slot

In veel applicaties is I/O een limiterende factor. Asynchronous programming is dan een handige tool om op een makkelijke manier de processortijd efficiënt te benutten. Het idee van een event loop bestaat natuurlijk al langer en ook andere talen zoals JavaScript maken hier gebruik van. Dat maakt het niet minder nuttig! Zo hielp het ons bij het indexeren van actieve pakketten.

P.S. Op de hoogte blijven van alle artikelen, updates, tips en trucs die op ons blog verschijnen? Volg ons via Facebook, Twitter, Instagram, RSS en e-mail!

Deel Deel Deel Deel

Geef een reactie

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *