2.17 Gelijktijdigheid en parallellisme
Overzicht en motivatie
Gelijktijdigheid (concurrency) is de kunst een programma te structureren als onafhankelijke taken die vooruitgang kunnen boeken zonder op elkaar te wachten. Parallellisme is die taken daadwerkelijk op hetzelfde moment uitvoeren op meerdere processoren. Het onderscheid is niet pedant. Gelijktijdigheid is een manier om code te organiseren zodat een trage netwerkaanroep niet het hele programma bevriest. Parallellisme is een manier om een grote berekening sneller af te maken door haar over cores te verspreiden. De twee verwarren leidt ertoe dat teams threads toevoegen in de hoop op snelheid en alleen bugs ontvangen.
Voor grote teams doet dit onderwerp ertoe omdat gelijktijdigheid is waar juistheid stilletjes sterft. Een enkele auteur die single-threaded code schrijft kan er regel voor regel over redeneren, maar zodra veel auteurs geheugen delen over threads, ontploft het aantal mogelijke interleavings, en een programma dat elke test doorstaat kan nog steeds één keer op een miljoen falen onder productiebelasting, niet met een luide crash maar met corrupte data, vastgelopen verzoeken en incidenten die niemand kan reproduceren. Dit hoofdstuk bouwt voort op de codegerichte focus van hoofdstuk 2.16 (performance engineering) en de fundamenten van de informatica uit hoofdstuk 2.13, en voedt de coördinatieproblemen van hoofdstuk 3.3 (gedistribueerde systemen), dat gelijktijdigheid over machines is met de extra wreedheid van een onbetrouwbaar netwerk.
Voor ondernemingen zijn concurrencybugs doorvoerbugs. Services met veel verkeer leven of sterven op hun vermogen duizenden gelijktijdige verzoeken af te handelen zonder te racen op gedeelde toestand, en één niet-gesynchroniseerde teller kan een grootboek onder belasting corrumperen. Voor de overheid zijn de inzetten juistheid en controleerbaarheid in systemen die decennialang draaien en veiligheid, uitkeringen of publieke registers raken. Een race in een belasting- of gezondheidssysteem is geen ongemak. Het is een fout antwoord dat iemand later aan een toezichthoudend orgaan moet uitleggen. In beide settings is het doel hetzelfde: maak het veilige pad de standaard, zodat de vele mensen die de code aanraken niet elk een concurrencyexpert hoeven te zijn.
Kernprincipes
- Gelijktijdigheid is structuur, parallellisme is uitvoering. Besluit welke je werkelijk nodig hebt voordat je naar threads grijpt.
- Gedeelde veranderlijke toestand is de vijand. Bijna elke concurrencybug herleidt naar twee taken die dezelfde veranderlijke data aanraken.
- Geef de voorkeur aan onveranderlijkheid en berichtuitwisseling. Data die niet kan veranderen kan niet worden geracet, en berichten verslaan gedeeld geheugen op veiligheid.
- Niet-determinisme is de kernmoeilijkheid. De bug die bij één run op duizend verschijnt is het hele probleem, geen randgeval.
- Begrens alles. Onbegrensde wachtrijen, threadaantallen en werk in uitvoering veranderen een piek in een storing.
- Modellen op hoger niveau verslaan ruwe locks. Actoren, kanalen en gestructureerde gelijktijdigheid geven veel auteurs een veilige standaard, en elke lock heeft een prijs.
- Test de interleavings, niet alleen het gelukkige pad. Deterministische tests kunnen een bug niet vangen die alleen een zeldzame volgorde onthult.
Aanbevelingen
Besluit of je gelijktijdigheid of parallellisme nodig hebt
Begin met het benoemen van het probleem. Als je service het grootste deel van zijn tijd wacht (op databases, netwerkaanroepen of schijf), heb je een I/O-gebonden werklast, en gelijktijdigheid is het antwoord: structureer de code zodat andere verzoeken doorgaan terwijl één verzoek wacht. Een enkele thread met async/await, of een kleine pool, kan duizenden wachtende verzoeken bedienen. Als je programma daarentegen CPU-gebonden is, door berekeningen malend met weinig wachten, dan koopt parallellisme over cores snelheid, en hier wordt het plafond bepaald door de wet van Amdahl (zie hoofdstuk 2.16): de seriële fractie begrenst je versnelling hoeveel cores je ook toevoegt. Meet in welk regime je zit voordat je ontwerpt.
Behandel gedeelde veranderlijke toestand als de vijand
Bijna elk concurrencydefect komt neer op dezelfde vorm: twee taken lezen en schrijven dezelfde veranderlijke data zonder een volgorde af te spreken. Dit is een race condition, en het produceert verloren updates, half geschreven objecten en waarden die invarianten schenden waarvan de code aannam dat ze veilig waren. De betrouwbaarste verdediging is minder gedeelde veranderlijke toestand te hebben. Geef elke taak zijn eigen data, geef kopieën door in plaats van referenties en beperk veranderlijke toestand tot één enkele eigenaar die anderen via berichten bereiken. Wanneer je werkelijk moet delen, maak het delen expliciet en klein, zodat een reviewer elke plek kan zien waar de toestand wordt aangeraakt.
Geef de voorkeur aan onveranderlijkheid en berichtuitwisseling als standaard
De veiligste gedeelde data is data die niet kan veranderen. Een onveranderlijk object kan, eenmaal geconstrueerd, door een willekeurig aantal threads worden gelezen met nul synchronisatie, omdat er niets is om op te racen. Maak onveranderlijkheid je standaard en veranderlijkheid de bewuste uitzondering. Wanneer taken moeten coördineren, geef dan de voorkeur aan berichtuitwisseling boven gedeeld geheugen: deel geen gemeenschappelijke variabele, maar laat één taak de waarde naar de ander sturen, wat de filosofie is achter het Go-spreekwoord “communiceer niet door geheugen te delen, deel geheugen door te communiceren”. Berichtuitwisseling verandert onzichtbare, volgordeafhankelijke bugs in expliciete, inspecteerbare datastroom, en die helderheid is bijna altijd de kosten per bericht waard in code die veel mensen onderhouden.
Grijp naar modellen op hoger niveau voor ruwe locks
Met de hand geschreven locking is in principe correct en in de praktijk rampzalig, omdat mensen slecht zijn in redeneren over elke interleaving. Geef de voorkeur aan modellen die veilige gelijktijdigheid de standaard maken. Het actormodel geeft elke actor privétoestand en een mailbox: actoren delen nooit geheugen en sturen alleen berichten, zodat hele klassen races verdwijnen. Communicating sequential processes (CSP), het model achter kanalen in talen als Go, laat onafhankelijke processen waarden over getypeerde kanalen doorgeven. Gestructureerde gelijktijdigheid koppelt de levensduur van gelijktijdige taken aan een lexicale scope, zodat taken het blok dat ze startte niet kunnen overleven en fouten zich voortplanten in plaats van te verdwijnen. Async/await laat je gelijktijdige, I/O-gebonden code in een sequentiële stijl schrijven. Elk daarvan verhoogt de vloer voor de gemiddelde auteur, wat een groot team nodig heeft.
Begrijp je geheugenmodel, atomiciteit en zichtbaarheid
Wanneer je wel geheugen deelt, bijten twee eigenschappen. Atomiciteit betekent dat een bewerking in één keer gebeurt of helemaal niet. Een gewone ophoging (x = x + 1) is niet atomair, omdat ze leest, optelt en schrijft als drie stappen die een andere thread kan onderbreken, waardoor tellers updates verliezen. Zichtbaarheid betekent dat een schrijfactie van de ene thread waarneembaar wordt voor een andere. Zonder goede synchronisatie kan een waarde die op de ene core is geschreven in een cache zitten die een andere niet ziet, zodat een thread eindeloos kan lussen op een vlag die al was gezet. Het geheugenmodel van je taal definieert wanneer schrijfacties zichtbaar worden en welke volgordes de compiler en CPU mogen herschikken, dus je kunt niet aannemen dat code in de volgorde draait waarin je haar schreef. Gebruik de atomaire types en synchronisatieprimitieven van de taal in plaats van je eigen lock-free schema te verzinnen.
Gebruik synchronisatieprimitieven bewust en ontwerp tegen deadlock
Wanneer delen onvermijdelijk is, grijp naar de juiste primitief en respecteer haar prijs. Een lock of mutex (mutual exclusion) laat één thread tegelijk een kritieke sectie binnengaan, maar serialiseert toegang, dus een hete lock wordt een knelpunt dat het voordeel van veel cores uitwist. Een semafoor beperkt hoeveel taken tegelijk mogen doorgaan, wat is hoe je een pool begrenst. Atomaire bewerkingen bieden lock-free updates voor eenvoudige waarden als tellers, goedkoper dan een lock maar makkelijk te misbruiken voor alles wat samengesteld is. Locks brengen drie klassieke faalwijzen. Een deadlock is wanneer taken in een cyclus op elkaar wachten en geen kan doorgaan, het schoolvoorbeeld zijnde twee threads die elk één lock vasthouden en de andere willen. Een livelock is wanneer taken op elkaar blijven reageren maar geen vooruitgang boeken. Uithongering (starvation) is wanneer een taak nooit een middel krijgt omdat anderen blijven voordringen. De disciplines die dit voorkomen zijn concreet: leg een globale lockvolgorde op, houd locks kort vast, voeg time-outs toe zodat een vastgelopen taak luid faalt, roep nooit onbekende code aan terwijl je een lock vasthoudt en gebruik eerlijke planning waar uithongering een risico is. Schrijf deze regels op, want een nieuwe auteur kan ze niet herontdekken uit de code alleen.
Begrens je wachtrijen, pools en werk in uitvoering met backpressure
Een onbegrensde wachtrij is een tijdbom. Onder een verkeerspiek komt werk sneller binnen dan het leegloopt, groeit de wachtrij zonder limiet, loopt het geheugen vol en sterft de service op een manier die eruitziet als een mysterieuze out-of-memory-crash in plaats van de overbelasting die het is. Begrens elke wachtrij, begrens elke threadpool en pas backpressure toe: wanneer het systeem vol is, signaleer stroomopwaarts om te vertragen of wijs werk snel af in plaats van oneindig werk te accepteren dat je niet kunt afmaken. Dimensioneer pools naar de werklast (grofweg het aantal cores voor CPU-gebonden werk, hoger voor I/O-gebonden werk waar threads vooral wachten) en behandel de grens als bewuste capaciteitsbeslissing. Dit sluit aan op de veerkrachtpatronen van hoofdstuk 3.3.
Gebruik dataparallellisme waar het werk vanzelfsprekend parallel is
Sommige problemen splitsen schoon: pas dezelfde bewerking toe op elk element van een grote dataset, zonder dat een element van een ander afhangt. Dit dataparallellisme is de vriendelijkste soort, omdat er weinig gedeelde toestand is om op te racen en de versnelling het aantal cores kan benaderen, zoals map-reduce-pipelines, parallelle arraybewerkingen en gevectoriseerde numerieke code allemaal tonen. Respecteer ook hier de wet van Amdahl: de samenvoeg- of reduce-stap is vaak serieel en begrenst je winst, en de overhead van splitsen kan bij kleine invoer domineren. Grijp ernaar wanneer het werk per element substantieel is en de elementen werkelijk onafhankelijk zijn. Anders is de eenvoudigste sequentiële versie vaak zowel snel genoeg als veel makkelijker correct te houden, een punt dat de constructiepraktijken van hoofdstuk 2.9 versterken.
Test en debug niet-deterministische code met opzet
Concurrencybugs zijn niet-deterministisch, dus gewone tests, die één interleaving draaien, missen ze meestal. Pak het probleem bewust aan met stress- en fuzztests die veel taken onder gerandomiseerde timing draaien om zeldzame volgordes te schudden. Grijp naar race-detectors en threadsanitisers, tooling die geheugentoegang instrumenteert om dataraces te vangen zelfs wanneer de foute interleaving deze run niet optrad. Gebruik waar je platform het biedt deterministische simulatie of gecontroleerde schedulers die een specifieke interleaving herhalen, waardoor een heisenbug reproduceerbaar wordt, en ontwerp zo dat je bij een productievastloper threadtoestanden en lockeigenaarschap kunt vastleggen, wat aansluit op de debugdiscipline van hoofdstuk 2.15. Geef boven alles de voorkeur aan ontwerpen (onveranderlijkheid, berichtuitwisseling, enkel eigenaarschap) die hele categorieën van deze bugs onmogelijk maken, want een bug die je niet kunt maken is er een die je nooit hoeft te debuggen.
Afwegingen: voor- en nadelen
| Aanpak | Voordelen | Nadelen |
|---|---|---|
| Gedeeld geheugen met locks | Snel per bewerking. Vertrouwd | Race-, deadlock- en zichtbaarheidsbugs. Moeilijk voor veel auteurs correct te houden |
| Onveranderlijkheid | Geen synchronisatie nodig. Triviaal thread-veilige leesacties | Kopieerkosten. Onhandig voor grote veranderlijke structuren |
| Berichtuitwisseling (actoren, kanalen) | Expliciete datastroom. Hele bugklassen verdwijnen | Overhead per bericht. Kan backpressure verbergen als wachtrijen onbegrensd zijn |
| Async/await | Goedkope gelijktijdigheid voor I/O-gebonden werk. Sequentieel ogende code | Geen parallellisme voor CPU-werk. Een taak blokkeren stalt anderen |
| Gestructureerde gelijktijdigheid | Heldere taaklevensduur. Fouten planten zich voort. Geen weggelekte taken | Nieuwer, in sommige ecosystemen minder beschikbaar |
| Dataparallellisme | Bijna lineaire versnelling op onafhankelijk werk | Plafond van Amdahl. Overhead domineert kleine invoer |
| Atomics / lock-free | Geen lockcontentie voor eenvoudige waarden | Extreem makkelijk subtiel fout te doen. Moeilijk te reviewen |
De centrale spanning is veiligheid tegenover ruwe snelheid, en de oplossing is eerst juistheid te kopen en prestaties alleen uit te geven waar meting bewijst dat het moet. Ruwe locking op gedeeld geheugen is het snelst per bewerking en het gevaarlijkst per regel code. Modellen op hoger niveau kosten een beetje doorvoer en leveren veel veiligheid en helderheid op, en voor code onderhouden door vele handen is die ruil beslist de moeite waard. Bewaar met de hand afgestemde lock-free gelijktijdigheid voor de kleine hotspots waar een profiler (hoofdstuk 2.16) bewijst dat de coördinatie-overhead ertoe doet, en houd zelfs die achter een goed geteste grens.
Vragen om met je team te bespreken
Is de werklast van je drukste service I/O-gebonden of CPU-gebonden, en komt je gelijktijdigheidsontwerp daarmee overeen? Teams voegen routinematig threadpools toe aan services die 95% van hun tijd wachten op een database, wat contentie oplevert maar geen doorvoer, of proberen een berekening te parallelliseren waarvan de seriële fractie elke versnelling begrenst. Het juiste ontwerp volgt uit het regime: async of een kleine pool voor wachtzwaar werk, echt parallellisme over cores voor rekenzwaar werk. Neem een profiel mee dat toont waar de tijd werkelijk heen gaat, geen aanname, en als de meeste tijd aan rekenen wordt besteed, meet dan de seriële fractie en laat de wet van Amdahl je het plafond vertellen. Het antwoord bepaalt of je naar async, een begrensde pool of dataparallellisme grijpt.
Wat is de standaard van je team voor het delen van toestand tussen taken, en is die veilig door constructie? In een groot team telt de standaard meer dan de uitzonderingen, omdat de meeste code wordt geschreven door mensen die geen concurrencyspecialisten zijn en kopiëren welk patroon er al is. Als de standaard gedeelde veranderlijke objecten bewaakt door ad hoc locks is, ben je één vergeten lock verwijderd van een race die maanden later in productie opduikt. Als de standaard onveranderlijkheid en berichtuitwisseling is, komen hele categorieën bugs nooit voor, en de zeldzame plek die werkelijk gedeeld geheugen nodig heeft valt op voor zorgvuldige review. Bespreek waar een nieuwe engineer vandaag naar zou grijpen, of je reviews een niet-gesynchroniseerde schrijfactie zouden vangen en hoe je het veilige pad het makkelijke maakt.
Hoe zou je een concurrencybug vinden, reproduceren en repareren die één keer op een miljoen verzoeken in productie verschijnt? Het eerlijke antwoord voor veel teams is dat ze het niet konden, omdat de bug verdwijnt wanneer ze kijken en hun tests altijd maar één goedaardige interleaving draaien. Dat zou je moeten verontrusten, want deze bugs corrumperen stilletjes data en ondermijnen vertrouwen. Praat over of je race-detectors en threadsanitisers in continuous integration draait, of je stresstest met gerandomiseerde timing en of je productieobserveerbaarheid thread- en lockstoestand vastlegt op het moment van een vastloper. De beste teams antwoorden door de meeste van zulke bugs onmogelijk te maken door hun keuze van model, zodat de resterende paar zeldzaam en beperkt zijn.
Waar in je systeem bestaat nog een onbegrensde wachtrij of een onbegrensde threadpool, en wat gebeurt ermee bij een plotselinge tienvoudige piek? Dit doet ertoe omdat onbegrensd werk in uitvoering het falen is dat zich voordoet als een mysterieuze out-of-memory-crash: werk komt sneller binnen dan het leegloopt, het geheugen loopt vol en de service sterft op een manier die eruitziet als een hardwarefout in plaats van de overbelasting die het is. De concurrerende overwegingen zijn echt, want een te laag ingestelde grens wijst legitiem verkeer af en een te hoge grens stelt de crash uit in plaats van hem te voorkomen, dus het getal is een capaciteitsbeslissing, geen gok. Neem een inventaris mee van elke wachtrij en pool, zijn huidige grens (of de erkenning dat hij er geen heeft), het backpressuregedrag wanneer hij vol raakt en belastingtestbewijs van hoe het systeem aan de rand degradeert. Voor een ondernemingsvloot kan één enkele onbegrensde wachtrij uitgroeien tot een storing van de hele vloot, en voor een overheidsplatform dat beschikbaar moet blijven voor burgers is gracieuze afwijzing met een duidelijke fout een dienstverplichting, dus de grens en zijn afwijzingspad horen in het capaciteitsplan en het runbook, niet in het geheugen van één engineer.
Wat is het beleid van je team voor het gebruik van gelijktijdigheidsmodellen op hoger niveau tegenover met de hand geschreven locks, en waar heb je uitzonderingen toegestaan? Het standaardmodel bepaalt hoe veilig de gemiddelde wijziging is, omdat de meeste auteurs geen concurrencyspecialisten zijn en het patroon kopiëren dat al bestaat: actoren, kanalen en gestructureerde gelijktijdigheid verhogen de vloer voor iedereen, terwijl ruwe locking in theorie correct is en in de praktijk een bron van deadlocks. De spanning is dat modellen op hoger niveau een beetje overhead per bericht of taak kosten, en een profiler zal af en toe bewijzen dat een heet pad met de hand afgestemde lock-free code nodig heeft, dus een algeheel verbod is even fout als een vrijbrief. Neem de lijst mee van plaatsen waar je onder de veilige standaard bent gegaan, het profileerbewijs dat elk rechtvaardigde en hoe elke uitzondering is afgeschermd achter een geteste grens en een gedocumenteerde lockvolgorde. In een grote onderneming houdt dit beleid duizenden bijdragers ervan af elk een onveilig schema opnieuw uit te vinden, en in een langlevend overheidssysteem laat het een reviewer jaren later begrijpen waarom een gevaarlijk patroon was toegestaan en bevestigen dat het nog gerechtvaardigd is.
Wanneer je besluit een berekening te parallelliseren, hoe meet je de seriële fractie, en wie is verantwoordelijk voor het bevestigen dat de versnelling echt is? Teams verspreiden routinematig een berekening over cores en vieren een getal dat een profiler nooit zou bevestigen, omdat de wet van Amdahl de winst begrenst tot het omgekeerde van de seriële fractie, hoeveel cores je ook toevoegt, en de overhead van splitsen en samenvoegen het voordeel voor kleine invoer volledig kan uitwissen. De concurrerende trek is dat parallellisme echte complexiteit en nieuw raceoppervlak toevoegt, dus de vraag is of de gemeten versnelling het correctheidsrisico rechtvaardigt dat je aanneemt. Neem een profiel mee dat het seriële deel isoleert, de invoergroottes waar parallellisme werkelijk wint en een benchmark voor en na op representatieve hardware in plaats van een hoopvolle schatting. Voor een onderneming die een grote rekenvloot betaalt, verandert een eerlijke analyse van de seriële fractie in bespaarde of verspilde hardware-uitgaven, en voor een overheidsorgaan verantwoordelijk voor de kosten van een publiek systeem moet degene die het parallelle ontwerp goedkeurde onder audit de meting kunnen tonen die het rechtvaardigde.
Sectorperspectief
Startup. Met een piepklein team en geen speelruimte koop je juistheid met structuur, niet met een concurrencyspecialist die je niet kunt aannemen. Grijp naar de ene veilige standaard die je taal geeft, async/await voor I/O-gebonden werk, één eigenaartaak of een actor voor elke gedeelde toestand, en sla met de hand afgestemde locking helemaal over. Een lost-update-race in een betalingspad kan je sneller laten zinken dan een gemiste functie, dus besteed de kleine hoeveelheid extra code om die klasse bug onmogelijk te maken en ga verder.
Kleinbedrijf. Je hebt niemand wiens taak gelijktijdigheid is, dus geef de voorkeur aan platforms en beheerde diensten die het voor je afhandelen: een databasetransactie, een gehoste wachtrij of het verzoekmodel van een framework verslaat threads die je met de hand onderhoudt. Behandel bij het evalueren van een tool “maakt dit gelijktijdigheid standaard veilig” als een vraag over kopen tegenover bouwen, en geef de voorkeur aan de optie waar een foute interleaving het record van een klant niet stilletjes kan corrumperen. Houd gedeelde veranderlijke toestand uit je eigen code waar een begrensde, beheerde dienst haar in plaats daarvan kan bewaren.
Grote onderneming. Over veel teams is het doel een huisstandaard die duizenden bijdragers veilig houdt: onveranderlijkheid en berichtuitwisseling als norm, modellen op hoger niveau boven ruwe locks, begrensde wachtrijen en pools met backpressure en een gedocumenteerde globale lockvolgorde. Codeer deze in engineeringstandaarden, dwing ze af met race-detectors en stresstests in CI en bestuur de uitzonderingen waar een profiler lock-free code rechtvaardigde zodat elke achter een geteste, beoordeelde grens blijft. Beheer gelijktijdigheidscapaciteit als zorg van de hele vloot, met wachtrijgrenzen en poolgroottes gekoppeld aan gemeten belasting.
Overheid. Juistheid en controleerbaarheid in systemen die decennialang draaien gaan boven ruwe doorvoer. Eis dat elke toestandsovergang wordt vastgelegd en herhaalbaar is, zodat een vermoedelijke race kan worden gereproduceerd en de oplossing aan een toezichthoudend orgaan kan worden bewezen, en houd AI-vrije deterministische paden voor beslissingen die uitkeringen, veiligheid of publieke registers raken. Aanbesteding moet eisen dat leveranciers hun gelijktijdigheidsmodel bekendmaken en bewijs van dekking door race-detectors en stresstests leveren, want een fout antwoord onder belasting in een publiek systeem is geen ongemak, maar iets wat een verantwoordelijke functionaris later moet uitleggen.
Voorbeelden
Startup. Een klein team levert een betalingsfunctie op en merkt dat rekeningsaldi onder belasting af en toe een paar cent afdrijven. De oorzaak is een gewone lees-wijzig-schrijfbewerking op een saldoveld vanuit gelijktijdige verzoekafhandelaars, een lost-update-race. In plaats van locks rond te strooien verplaatsen ze het saldo van elke rekening achter één enkele eigenaartaak die debits en credits als berichten verwerkt, één voor één. De afdrijving verdwijnt, de code wordt makkelijk om over te redeneren en ze voegen een stresstest toe die duizenden gelijktijdige overboekingen afvuurt om de oplossing te bewaken. Eén structurele wijziging, een hele klasse bug afgeschaft.
Grote onderneming. Een bestelservice met veel doorvoer die tienduizenden verzoeken per seconde afhandelt, lijdt aan periodieke latentiepieken en incidentele out-of-memory-crashes tijdens verkeerspieken. Onderzoek vindt een onbegrensde werkwachtrij achter een threadpool die zonder limiet groeit zodra de vraag de capaciteit overschrijdt. Het team begrenst de wachtrij, begrenst de pool op een grootte gekoppeld aan het aantal cores en voegt backpressure toe die overtollige belasting snel afwijst met een duidelijke fout. De doorvoer wordt voorspelbaar, de crashes stoppen en een hete lock op een gedeelde cache wordt pas vervangen door een lock-free structuur nadat een profiler bewijst dat de contentie echt is. Veilige standaarden voor de vele auteurs, afgestemde gelijktijdigheid alleen waar gemeten.
Overheid. Een nationaal uitkeringsplatform draait decennialang en moet controleerbare, correcte resultaten produceren, zelfs onder gelijktijdige zaakupdates. Het team kiest onveranderlijkheid en berichtuitwisseling als huisstandaard, beperkt elk stuk veranderlijke toestand tot één eigenaar en legt een globale lockvolgorde op waar locks blijven, alles vastgelegd in de engineeringstandaarden. Ze draaien threadsanitisers en gerandomiseerde stresstests in de pipeline en ontwerpen zo dat elke toestandsovergang wordt vastgelegd en herhaalbaar is voor toezicht, waardoor ze de oplossing kunnen reproduceren en bewijzen wanneer een zeldzame interleaving wordt vermoed. Juistheid en controleerbaarheid worden behandeld als eersteklas vereisten, niet als prestatiebijzaken.
Zakelijke onderbouwing: motivatie, ROI en TCO
Het rendement van gedisciplineerde gelijktijdigheid toont zich als incidenten die nooit gebeuren. Eén productierace kan data over duizenden records corrumperen, en de kosten omvatten zowel de engineeringuren om een bug te vinden die zich verbergt wanneer hij wordt geobserveerd als de veel grotere kosten van het reconciliëren van slechte data, het informeren van getroffen gebruikers en het herbouwen van vertrouwen. Dit behoren tot de duurste defecten om te diagnosticeren juist omdat ze niet-deterministisch zijn, dus het najagen van één heisenbug kan de inspanning van vooraf een veilig model kiezen in de schaduw stellen.
Het voordeel toont zich ook als doorvoer en kosten. Gelijktijdigheid goed dimensioneren laat een service veel meer belasting aan op dezelfde hardware, een terugkerende besparing voor een grote vloot, terwijl backpressure en begrensde wachtrijen de cascaderende storingen voorkomen die een verkeerspiek in een publiek incident veranderen. De total cost of ownership is bescheiden en vooral cultureel: je investeert in een huisstijl (onveranderlijkheid, berichtuitwisseling, gestructureerde gelijktijdigheid), in tooling (race-detectors, threadsanitisers, stressharnassen in CI) en in standaarden die lockvolgorde en begrenzing codificeren. Het alternatief is een codebase waar juistheid ervan afhangt dat elke auteur voor altijd een expert is, wat geen groeiend team kan volhouden. Maak de zakelijke onderbouwing voor het bestuur in hun eenheden: vertaal een voorkomen race in vermeden datacorruptie-incidenten, backpressure in voorkomen storingen en een veilige standaard in bespaarde onboardingtijd.
Antipatronen en valkuilen
- Threads toevoegen voor snelheid bij I/O-gebonden werk. Meer threads op een wachtzware service kopen contentie, geen doorvoer.
- Gedeelde veranderlijke toestand overal. Elke thread die elk object muteert maakt juistheid een kwestie van geluk die geen reviewer kan verifiëren.
- Onbegrensde wachtrijen en pools. Een piek laat de wachtrij groeien tot het geheugen sterft. De crash lijkt mysterieus maar is gewone overbelasting.
- Ad hoc locking zonder globale volgorde. Locks in verschillende volgorden genomen over de codebase lopen vast onder belasting.
- Aannemen dat code in geschreven volgorde draait. Het geheugenmodel negeren, zodat een zichtbaarheidsbug een thread laat ronddraaien op een verouderde waarde.
- Zelfgebouwde lock-free slimmigheid. Eigen lock-free schema’s zijn bijna altijd subtiel fout en vrijwel onmogelijk te reviewen.
- Alleen de gelukkige interleaving testen. Deterministische tests slagen terwijl de ene volgorde op een miljoen productie corrumpeert.
- Onbekende code aanroepen terwijl je een lock vasthoudt. Een callback die blokkeert of opnieuw binnenkomt verandert een kritieke sectie in een deadlock.
Volwassenheidsmodel
- Niveau 1, Initiëren: Gelijktijdigheid is ad hoc en reactief. Threads en locks worden op instinct toegevoegd, gedeelde veranderlijke toestand is overal en wachtrijen zijn onbegrensd. Race conditions verschijnen als onreproduceerbare productie-incidenten die niemand kan diagnosticeren, en er is geen tooling om ze te vangen.
- Niveau 2, Ontwikkelen: Sommige teams hebben basispraktijken geleerd: ze gebruiken locks met meer zorg en begrenzen hun meest voor de hand liggende wachtrijen. Er is informeel bewustzijn van races en deadlocks, en een paar kritieke paden krijgen extra toezicht. De praktijk is inconsistent over teams, testen is nog vooral single-interleaving en veilige patronen leven in individuen in plaats van op papier.
- Niveau 3, Standaardiseren: De organisatie heeft een gedocumenteerde huisstijl organisatiebreed gehandhaafd: onveranderlijkheid en berichtuitwisseling als standaard, modellen op hoger niveau boven ruwe locks, begrensde wachtrijen en pools met backpressure en een gedocumenteerde globale lockvolgorde. Race-detectors en stresstests draaien in CI, en gelijktijdigheidskeuzes volgen uit of werk I/O-gebonden of CPU-gebonden is.
- Niveau 4, Beheersen: De organisatie meet en beheerst haar gelijktijdigheidshouding aan de hand van uitgangswaarden. Ze volgt dekking door race-detectors en threadsanitisers over services, registreert wachtrijdiepte, lockwachttijd, poolverzadiging en afwijzingspercentages als bewaakte statistieken en test de degradatiecurve onder belasting zodat elke grens een met data onderbouwde capaciteitsbeslissing is. Concurrencyincidenten worden geteld en getrend, seriële fracties van geparallelliseerde werklasten worden gemeten tegen de werkelijk bereikte versnelling, en go/no-go-beslissingen over nieuwe ontwerpen rusten op dat bewijs in plaats van op instinct.
- Niveau 5, Orkestreren: Veilige gelijktijdigheid is het pad van de minste weerstand voor elke auteur, en de praktijk wordt continu verbeterd en is over de organisatie geïntegreerd. Hele bugklassen zijn onmogelijk door constructie, hotspots worden alleen afgestemd waar profileren het bewijst en deterministische replay maakt de zeldzame resterende bug reproduceerbaar. Juistheid en controleerbaarheid zijn continu verdedigde eigenschappen, capaciteitsgrenzen passen zich aan waargenomen belasting aan en de standaarden evolueren naarmate het platform en de werklast verschuiven.
Ideeën voor discussie
- Als je je drukste service vandaag zou auditen, hoeveel van haar toestand is gedeeld en veranderlijk, en hoeveel van dat delen is werkelijk nodig?
- Wat is het standaardantwoord van je team wanneer iemand wil dat twee taken coördineren, en zou je liever hebben dat het onveranderlijkheid of berichtuitwisseling was?
- Waar verbergen zich nog onbegrensde wachtrijen of onbegrensde pools in je systeem, en wat zou ermee gebeuren bij een plotselinge tienvoudige verkeerspiek?
- Bevatten je continuous-integration-runs een race-detector of threadsanitiser, en wanneer ving er voor het laatst een iets voor productie?
- Wat is voor je meest geparallelliseerde werklast de seriële fractie, en begrenst de wet van Amdahl de versnelling die je werkelijk najaagt?
- Kan je team een interleavingbug van één op een miljoen op verzoek reproduceren, en wat zou nodig zijn om daar te komen?
Belangrijkste inzichten
- Gelijktijdigheid structureert een programma als onafhankelijke taken. Parallellisme voert ze tegelijk uit. Besluit welke je nodig hebt voordat je threads toevoegt.
- Gedeelde veranderlijke toestand is de wortel van bijna elke concurrencybug. Geef de voorkeur aan onveranderlijkheid en berichtuitwisseling als veilige standaarden voor veel auteurs.
- Grijp naar modellen op hoger niveau (actoren, kanalen, gestructureerde gelijktijdigheid, async/await) voor met de hand geschreven locks, die in theorie correct en in de praktijk gevaarlijk zijn.
- Begrijp atomiciteit, zichtbaarheid en je geheugenmodel. Gebruik de juiste primitief, houd locks kort vast en leg een globale lockvolgorde op om deadlock, livelock en uithongering te voorkomen.
- Begrens elke wachtrij en pool en pas backpressure toe, zodat een piek gracieus degradeert in plaats van te crashen (hoofdstuk 3.3).
- Test de interleavings met opzet met race-detectors, stresstests en replay (hoofdstuk 2.15), en respecteer de wet van Amdahl bij het parallelliseren (hoofdstuk 2.16).
- Voor ondernemingen is dit doorvoer en voorkomen incidenten. Voor de overheid is het juistheid en controleerbaarheid in langlevende systemen.
Referenties en verder lezen
- Brian Goetz et al., Java Concurrency in Practice (atomicity, visibility, the memory model, and safe publication).
- Herb Sutter, “The Free Lunch Is Over” (why software must embrace concurrency as clock speeds plateau).
- Leslie Lamport, “Time, Clocks, and the Ordering of Events in a Distributed System” (ordering and the foundations of concurrent reasoning).
- C. A. R. Hoare, “Communicating Sequential Processes” (Communications of the ACM, 1978): the CSP model behind channels.
- Carl Hewitt, Peter Bishop, and Richard Steiger, “A Universal Modular Actor Formalism for Artificial Intelligence” (the origin of the actor model).
- Edsger W. Dijkstra, “Cooperating Sequential Processes” (semaphores, mutual exclusion, and the deadlock problem).
- Maurice Herlihy and Nir Shavit, The Art of Multiprocessor Programming (locks, atomics, and lock-free data structures).
- Nathaniel J. Smith, “Notes on Structured Concurrency, or: Go Statement Considered Harmful” (the case for structured concurrency).
- Martin Kleppmann, Designing Data-Intensive Applications (concurrency and consistency where memory meets distributed systems).
- Gene M. Amdahl, “Validity of the Single Processor Approach to Achieving Large-Scale Computing Capabilities” (1967): the origin of Amdahl’s law.