Naar de inhoud
maarten.
Alle posts

Python async, threads of processen kiezen

Maarten Soetens 13 min lezen

Asyncio, threads en processen maken het mogelijk om meerdere taken tegelijk uit te voeren, maar verschillen sterk in uitvoering, foutgedrag en beheer van gedeelde gegevens. Je leest hoe je de juiste aanpak kiest voor I/O-gebonden en processorintensieve code, en welke gevolgen die keuze heeft voor de architectuur van een Python-applicatie.

Concurrency en parallelisme in Python zijn niet hetzelfde

Concurrency betekent dat een programma meerdere taken beheert die elkaar afwisselen of overlappen. Parallelisme betekent dat taken daadwerkelijk op hetzelfde moment worden uitgevoerd. Dat onderscheid bepaalt welke Python-aanpak geschikt is. Een asyncio-eventloop wisselt doorgaans tussen taken op één thread. Threads kunnen wachten op I/O terwijl andere threads verdergaan, maar Python-code wordt door de Global Interpreter Lock meestal niet gelijktijdig op meerdere cores uitgevoerd. Afzonderlijke processen kunnen dat wel.

Begin daarom niet met de vraag hoeveel taken tegelijk moeten lopen, maar met de vraag waar die taken op wachten. Bij netwerkverzoeken, databaseaanroepen en bestandstoegang is de vertraging vaak I/O. Terwijl één taak wacht, kan een andere voortgang boeken. Bij beeldverwerking, compressie of grote berekeningen is de processor zelf de beperking; meer gelijktijdige taken op dezelfde thread maken het werk dan niet sneller.

Meet het bestaande gedrag voordat je een concurrency-model kiest. Houd rekening met wachttijd, CPU-gebruik, geheugengebruik en het aantal gelijktijdige verbindingen. Een applicatie kan bijvoorbeeld weinig CPU gebruiken maar toch traag reageren doordat zij verzoeken serieel afhandelt. Een andere kan juist volledig CPU-belast zijn. Zonder dat onderscheid leidt een overstap naar async of multiprocessing vaak tot meer codecomplexiteit zonder relevante verbetering.

Asyncio voor veel gelijktijdige I/O-taken

Asyncio is geschikt wanneer een applicatie veel I/O-taken moet beheren en de gebruikte bibliotheken asynchroon samenwerken. Een coroutine geeft de controle bij een await terug aan de eventloop. Die kan intussen een andere coroutine uitvoeren, bijvoorbeeld terwijl een HTTP-verzoek, databasequery of socketoperatie wacht. Daardoor kan één thread een groot aantal netwerkverbindingen afhandelen zonder voor elke verbinding een aparte thread te maken.

De winst hangt af van consequent gebruik van asynchrone interfaces. Een blokkerende functie die rechtstreeks vanuit een coroutine wordt aangeroepen, houdt de eventloop stil. Tijdens die blokkade lopen andere coroutines op die loop niet verder. Veelvoorkomende oorzaken zijn synchrone netwerkclients, zware bestandsbewerkingen en berekeningen die lang achter elkaar op de eventloop draaien. Gebruik voor zulke onderdelen een asynchrone bibliotheek of verplaats het blokkerende werk naar een thread- of procespool.

Asyncio vraagt ook om expliciet beheer van taken. Maak taken niet onbeperkt aan wanneer inkomende verzoeken sneller binnenkomen dan externe diensten ze verwerken. Beperk gelijktijdigheid met bijvoorbeeld een semaphore of een begrensde queue. Denk daarnaast aan time-outs en het annuleren van taken wanneer een verzoek niet langer nodig is. Asyncio past goed bij I/O-intensieve servers, maar maakt synchrone afhankelijkheden en ontbrekende grenzen niet vanzelf efficiënt.

Python-threads voor blokkerende I/O en bestaande bibliotheken

Threads zijn vaak praktisch wanneer code wacht op I/O, maar de gebruikte bibliotheken geen asynchrone interface bieden. Een thread kan geblokkeerd wachten op een netwerkantwoord of bestand, terwijl andere threads hun werk uitvoeren. Daarmee is het mogelijk bestaande synchrone code te behouden en toch meerdere onafhankelijke I/O-taken tegelijk te starten. Dit is regelmatig eenvoudiger dan een volledige applicatie ombouwen naar coroutines.

Voor Python-code die intensief rekent, leveren threads doorgaans geen parallelle uitvoering van bytecode op. De Global Interpreter Lock zorgt ervoor dat per interpreter meestal maar één thread tegelijk Python-bytecode uitvoert. Sommige bibliotheken geven de GIL tijdens native berekeningen of I/O vrij; in zulke gevallen kunnen threads alsnog voordeel bieden. Dat gedrag is afhankelijk van de gebruikte implementatie en bibliotheek, en moet met representatieve belasting worden gemeten.

Threads delen hetzelfde geheugen en kunnen daarom gegevens uitwisselen zonder serialisatie. Dat is handig, maar maakt gelijktijdige mutaties risicovol. Gebruik threadveilige queues, locks of immutable gegevens in plaats van aan te nemen dat een bewerking vanzelf atomair is. Beperk bovendien het aantal threads: een thread heeft geheugen- en schedulingkosten, en een grote pool kan een database of externe API overbelasten. Threads passen vooral wanneer blocking I/O en compatibiliteit zwaarder wegen dan maximale controle over uitvoering.

Multiprocessing voor processorintensief werk

Multiprocessing voert werk uit in aparte besturingssysteemprocessen. Elk proces heeft een eigen Python-interpreter en geheugenruimte, waardoor CPU-intensieve Python-code meerdere processorkernen kan benutten. Denk aan het transformeren van grote datasets, berekeningen op afzonderlijke bestanden of het genereren van onafhankelijke resultaten. Een procespool voorkomt dat de applicatie voor iedere taak zelf een proces moet starten en beheren.

Parallel rekenen is niet gratis. Argumenten en resultaten moeten vaak tussen processen worden geserialiseerd en overgedragen. Grote objecten kunnen daardoor veel tijd en geheugen kosten, ook als de berekening zelf snel is. Een procespool werkt het best wanneer taken voldoende omvangrijk zijn ten opzichte van die overdracht, en wanneer taken grotendeels onafhankelijk kunnen worden uitgevoerd. Deel waar mogelijk compacte gegevens of verwijzingen naar gedeelde opslag in plaats van omvangrijke objectgrafen.

Processen brengen ook operationele aandachtspunten mee. Het starten van processen verschilt tussen platformen, en code die processen aanmaakt moet correct zijn afgeschermd voor de gebruikte startmethode. Houd rekening met poolbeheer, beëindiging bij fouten en het opruimen van subprocessen. Ook kunnen meerdere processen gezamenlijk meer geheugen gebruiken dan één threadpool. Multiprocessing is daarom geen algemene versneller voor elke taak, maar een passende keuze wanneer metingen aantonen dat Python-code de CPU begrenst en de rekentaak efficiënt te verdelen is.

Gedeelde toestand beheersen met locks, queues en processen

De manier waarop uitvoeringseenheden gegevens delen, beïnvloedt de betrouwbaarheid van de applicatie. Threads delen standaard hetzelfde geheugen. Twee threads kunnen daardoor tegelijk dezelfde datastructuur lezen en aanpassen. Als de volgorde van die bewerkingen verandert, kan een resultaat verloren gaan of een invariant worden geschonden. De GIL voorkomt zulke fouten niet: samengestelde bewerkingen bestaan vaak uit meerdere stappen en kunnen door andere threads worden onderbroken.

Locks beschermen kritieke secties, maar langdurig of verkeerd genest lockgebruik kan prestaties verminderen of deadlocks veroorzaken. Een thread die wacht op een lock houdt mogelijk middelen vast die een andere thread nodig heeft. Waar mogelijk is communicatie via een threadveilige queue overzichtelijker: één onderdeel bezit de toestand en andere onderdelen sturen berichten of opdrachten. Dat beperkt gelijktijdige mutaties en maakt de stroom van werk beter te volgen.

Processen delen hun gewone geheugen niet op dezelfde manier. Communicatie verloopt doorgaans via queues, pipes, gedeeld geheugen of externe opslag. Dat dwingt tot expliciete grenzen, maar brengt serialisatie, kopieerkosten en synchronisatie met zich mee. Asyncio heeft meestal één thread die coroutines afwisselt, waardoor gedeelde toestand eenvoudiger te beheren kan zijn zolang coroutines geen controle afstaan tijdens een wijziging. Ook daar blijven invarianten belangrijk, zeker wanneer taken tussen meerdere stappen een await uitvoeren. Kies een model waarin eigenaarschap van gegevens helder is, niet alleen een model waarin de eerste benchmark snel lijkt.

Foutafhandeling, annulering en levenscyclus van taken

Concurrency verandert hoe fouten door een applicatie lopen. In synchrone code staat de fout vaak direct in de aanroepketen. Bij asyncio, threads en processen kan een taak falen terwijl de aanroeper andere taken uitvoert. De applicatie moet daarom bepalen waar uitzonderingen worden opgevangen, hoe resultaten worden opgehaald en wat er gebeurt met werk dat afhankelijk is van een mislukte taak.

Bij asyncio is annulering onderdeel van de normale levenscyclus. Een time-out of een client die de verbinding verbreekt kan een coroutine annuleren. Code moet annulering niet ongemerkt inslikken en moet noodzakelijke opruimacties uitvoeren, bijvoorbeeld het sluiten van een verbinding. Een task group kan taken als één samenhangende eenheid beheren: bij een fout kunnen andere taken worden beëindigd en kunnen fouten gezamenlijk worden gerapporteerd. Zonder expliciet taakbeheer blijven achtergrondtaken soms draaien nadat het oorspronkelijke verzoek al is afgehandeld.

Bij threads en procespools worden fouten meestal zichtbaar wanneer de hoofdthread het resultaat van een future opvraagt. Als resultaten nooit worden gecontroleerd, blijven fouten verborgen. Een werkende thread kan bovendien niet altijd veilig van buitenaf worden afgebroken; gebruik stopvlaggen, time-outs en begrensde wachttijden. Processen zijn afzonderlijk te beëindigen, maar abrupt stoppen kan gedeelde resources of half afgemaakte uitvoer achterlaten. Definieer dus per taak wat herhaalbaar, annuleerbaar en herstelbaar is. Een concurrency-model zonder duidelijk beleid voor fouten en opruimen leidt tot stilstaande taken en onvolledige resultaten.

Een applicatie met meerdere concurrency-modellen opbouwen

Een Python-applicatie hoeft niet overal hetzelfde concurrency-model te gebruiken. Een asynchrone webserver kan netwerkverzoeken met asyncio afhandelen, een threadpool gebruiken voor een bestaande blocking client en een procespool inzetten voor zware berekeningen. Die combinatie is alleen beheersbaar wanneer de grenzen tussen onderdelen expliciet zijn. Bepaal welke component verantwoordelijk is voor het starten, begrenzen en beëindigen van elk soort werk.

Voorkom dat lagen ongemerkt modellen door elkaar halen. Een synchrone functie die een eventloop start vanuit code waar al een loop actief is, kan fouten of moeilijk reproduceerbaar gedrag opleveren. Evenzo kan een coroutine die een blocking functie aanroept de hele loop ophouden. Maak adapters die een helder contract bieden: welke invoer wordt geaccepteerd, waar de uitvoering plaatsvindt, hoe annulering werkt en welk type fout wordt teruggegeven. Houd de overdracht tussen modellen zo klein mogelijk.

Een nuttige architectuur scheidt taakcoördinatie van taakuitvoering. De coördinator kan bijvoorbeeld een aanvraag valideren en vervolgens werk naar een begrensde pool sturen. De pool hoeft niet te weten hoe een HTTP-verzoek wordt afgehandeld; de weblaag hoeft niet te beheren hoe CPU-werk over processen wordt verdeeld. Dit voorkomt dat details van threads, loops en process pools door het hele programma lekken. Bij het combineren van modellen moeten ook loggingcontext, tracinggegevens en shutdown-signalen de grenzen passeren. Zonder die afspraken worden fouten lastig te herleiden en kan het stoppen van één onderdeel resources van andere onderdelen achterlaten.

Backpressure en limieten voor gelijktijdig werk

Meer gelijktijdige taken betekenen niet automatisch een hogere verwerkingscapaciteit. Als een applicatie verzoeken sneller accepteert dan zij ze kan afhandelen, groeit de wachtrij en stijgt het geheugengebruik. Externe afhankelijkheden krijgen ondertussen meer verbindingen of aanvragen dan zij aankunnen. Dit speelt bij asyncio net zo goed als bij threads en processen; het verschil zit vooral in de kosten en het beheer van wachtende taken.

Backpressure voorkomt dat werk onbeperkt blijft opstapelen. Gebruik begrensde queues, semaphores of limieten op het aantal verbindingen, en bepaal wat er gebeurt wanneer een grens is bereikt. Een webservice kan nieuwe verzoeken tijdelijk weigeren, een producent laten wachten of werk duurzaam opslaan voor latere verwerking. De juiste keuze hangt af van de betekenis van het werk: een interactief verzoek heeft andere eisen dan een batchtaak die veilig in een queue kan blijven staan.

Stem limieten af op alle betrokken resources. Een pool van honderd threads kan bijvoorbeeld meer databaseverbindingen aanvragen dan de database beschikbaar heeft. Een grote procespool kan geheugen uitputten, terwijl een onbeperkt aantal coroutines nog steeds sockets, file descriptors of quota van een API kan verbruiken. Meet wachtrijlengte en wachttijd naast de uitvoeringstijd. Een oplopende wachtrij wijst vaak op een capaciteitsprobleem of een te ruim ingestelde instroom, niet op een tekort aan concurrency. Limieten maken overbelasting zichtbaar en houden de belasting van afhankelijke systemen beheersbaar.

Concurrency testen en problemen in productie herkennen

Concurrencyproblemen zijn vaak afhankelijk van timing en verschijnen niet bij iedere uitvoering. Een test die één keer slaagt, bewijst daarom niet dat gedeelde toestand veilig is of dat shutdown correct verloopt. Test scenario’s waarin taken tegelijk dezelfde gegevens aanpassen, een afhankelijkheid traag reageert, een time-out optreedt of een worker onverwacht faalt. Controleer niet alleen het eindresultaat, maar ook of open verbindingen, processen en threads na afloop worden opgeruimd.

Gebruik voor prestatiemetingen representatieve workloads. Meet doorvoer én latency, inclusief hoge percentielen, omdat een gemiddelde wachttijd lange vertragingen kan verbergen. Houd CPU-gebruik, geheugen, actieve threads of processen, eventloopvertraging en wachtrijlengte bij. Vergelijk dezelfde taak onder dezelfde omstandigheden en neem opstarttijd en gegevensoverdracht mee. Een multiprocessing-test die de berekening versnelt maar veel tijd verliest aan serialisatie kan in productie slechter uitpakken.

Bij asyncio is eventloopvertraging een bruikbare aanwijzing dat blocking code of lange berekeningen de loop bezet houden. Bij threads zijn vastlopende workers, lockwachttijd en uitgeputte pools relevante signalen. Bij processen zijn herstarts, geheugen per worker en de tijd voor het ophalen van resultaten van belang. Leg taakidentificatie en context vast in logs, zodat een fout te koppelen is aan het verzoek of batchwerk dat haar veroorzaakte. Test ook gecontroleerde afsluiting: nieuwe taken stoppen met binnenkomen, lopend werk krijgt een afgesproken behandeling en resources worden gesloten. Zo wordt zichtbaar of het gekozen model niet alleen tijdens normaal gebruik, maar ook bij vertragingen en storingen beheersbaar blijft.

Veelgestelde vragen

Hoe bepaal ik het juiste aantal workers voor een Python-pool?

Bepaal het aantal workers met metingen onder representatieve belasting; er is geen vaste waarde die voor elke applicatie goed werkt. Begin met een kleine pool en verhoog het aantal stapsgewijs terwijl je doorvoer, wachttijd, geheugengebruik en belasting van externe diensten volgt.

  • Bij I/O-werk kan het aantal workers hoger liggen dan het aantal CPU-kernen, maar begrens het ook op basis van verbindingen en API-limieten.
  • Bij CPU-werk is het aantal beschikbare processorkernen een bruikbaar startpunt.
  • Test ook piekbelasting en controleer of extra workers de prestaties werkelijk verbeteren.

Hoe test ik race conditions in Python-threads?

Test race conditions door gedeelde toestand onder gecontroleerde concurrentie te belasten en expliciet te controleren of invarianten behouden blijven. Een test die één keer slaagt, bewijst niet dat de code veilig is: timingproblemen kunnen sporadisch optreden en zijn afhankelijk van de belasting.

  • Start veel gelijktijdige taken die dezelfde toestand aanpassen en controleer daarna de verwachte uitkomst.
  • Gebruik synchronisatiehulpmiddelen zoals barriers of events om kritieke uitvoeringsvolgordes reproduceerbaar te maken.
  • Voer stresstests herhaaldelijk uit en controleer ook fouten en time-outs.

Gebruik zulke tests naast een ontwerp met duidelijk eigenaarschap van gegevens; tests alleen vervangen geen veilige synchronisatie.

Hoe test ik asyncio-code zonder een echte API of database?

Test asyncio-code zonder echte externe diensten door de netwerk- of databaseclient te vervangen door een testdouble met asynchrone methoden. Die kan vooraf bepaalde resultaten teruggeven of juist een fout, vertraging of time-out simuleren.

  • Controleer dat coroutines de client daadwerkelijk awaiten en dat fouten correct worden afgehandeld.
  • Test annulering, bijvoorbeeld wanneer een verzoek wordt afgebroken terwijl een taak wacht.
  • Gebruik voor een kleiner aantal integratietests een lokale testserver of tijdelijke testdatabase.

Zo blijven de meeste tests snel en voorspelbaar, terwijl enkele integratietests controleren of de echte bibliotheken goed samenwerken.

Hoe gebruik ik ProcessPoolExecutor veilig op Windows?

Gebruik ProcessPoolExecutor op Windows door processtartende code af te schermen met een hoofdprogramma-guard en werkfuncties op module-niveau te definiëren. Windows start processen doorgaans op een manier waarbij de Python-module opnieuw wordt ingelezen; code die bij het importeren meteen een pool aanmaakt, kan daardoor processen blijven starten.

  • Plaats het starten van de applicatie of pool achter if __name__ == "__main__":.
  • Geef taken en argumenten mee die serialiseerbaar zijn.
  • Sluit de pool netjes af en test het gedrag met de daadwerkelijke startmethode van de doelomgeving.

Hoe voorkom ik te veel Python-processen bij Gunicorn en een procespool?

Voorkom een onverwacht groot aantal processen door het aantal webworkers te vermenigvuldigen met het aantal workers in iedere procespool. Als elke webworker zijn eigen pool start, kan het totale aantal achtergrondprocessen veel hoger uitvallen dan het ingestelde poolaantal.

  • Bereken het totale aantal processen voor de volledige deployment, niet alleen per applicatieproces.
  • Houd rekening met CPU-kernen, geheugen en andere diensten op dezelfde machine.
  • Meet onder belasting en pas zowel het aantal webworkers als poolworkers aan.

Beheer de pool bovendien op een passende plek in de levenscyclus van de applicatie, zodat workers niet onbedoeld opnieuw worden aangemaakt of na afsluiten blijven bestaan.

Portret van Maarten

Maarten

Freelance developer in Nijmegen

Even kennismaken?

Vertel kort wat er speelt. Dan hoor je wat er kan, wat ik anders zou doen en waar AI bij jou wél en niet iets toevoegt. Vrijblijvend.

[email protected]
Het kantoor in Nijmegen
© 2026 maarten.online Sitemap Privacy Algemene voorwaarden
Het kantoor in Nijmegen

Maarten.

Freelance developer in Nijmegen. Liever direct contact? Dat kan ook.

Kennismaken

Laat je gegevens achter, dan kijken we of het klikt. Vrijblijvend en zonder verkooppraat.

Maarten

Stuur een bericht via WhatsApp

Hoi! Waar kan ik je mee helpen?

nu