2.17 Samtidighed og parallelisme
Overblik og motivation
Samtidighed (concurrency) er kunsten at strukturere et program som uafhængige opgaver, der kan gøre fremskridt uden at vente på hinanden. Parallelisme er faktisk at eksekvere de opgaver i samme øjeblik på flere processorer. Forskellen er ikke pedantisk. Samtidighed er en måde at organisere kode på, så et langsomt netværkskald ikke fryser hele programmet; parallelisme er en måde at færdiggøre en stor beregning hurtigere ved at sprede den over kerner. At forveksle de to får teams til at tilføje tråde i håb om hastighed og kun få fejl.
For store teams betyder dette emne noget, fordi samtidighed er, hvor korrekthed stille dør. En enkelt forfatter, der skriver enkelttrådet kode, kan ræsonnere om den linje for linje, men i det øjeblik mange forfattere deler hukommelse på tværs af tråde, eksploderer antallet af mulige interleavinger, og et program, der består hver test, kan stadig fejle én gang i en million under produktionsbelastning, ikke med et højlydt nedbrud, men med korrumperede data, hængende anmodninger og hændelser, ingen kan reproducere. Dette kapitel bygger på kodefokus i kapitel 2.16 (ydeevneoptimering) og de datalogiske grundlag i kapitel 2.13, og det fodrer ind i koordinationsproblemerne i kapitel 3.3 (distribuerede systemer), som er samtidighed på tværs af maskiner med den tilføjede grusomhed fra et upålideligt netværk.
For virksomheder er samtidighedsfejl gennemløbsfejl. Højtrafikerede tjenester lever eller dør på deres evne til at håndtere tusindvis af samtidige anmodninger uden at konkurrere om delt tilstand, og en enkelt usynkroniseret tæller kan korrumpere en hovedbog under belastning. For det offentlige er indsatsen korrekthed og reviderbarhed i systemer, der kører i årtier og rører sikkerhed, ydelser eller offentlige registre. Et kapløb i et skatte- eller sundhedssystem er ikke en ulejlighed; det er et forkert svar, nogen senere må forklare for et tilsynsorgan. I begge sammenhænge er målet det samme: gør den sikre sti til standarden, så de mange mennesker, der rører koden, ikke hver især skal være samtidighedseksperter.
Grundprincipper
- Samtidighed er struktur; parallelisme er eksekvering. Beslut, hvilken af dem du faktisk har brug for, før du griber til tråde.
- Delt mutérbar tilstand er fjenden. Næsten enhver samtidighedsfejl kan spores tilbage til to opgaver, der rører de samme foranderlige data.
- Foretræk uforanderlighed og beskedudveksling. Data, der ikke kan ændres, kan ikke konkurreres om, og beskeder slår delt hukommelse for sikkerhed.
- Ikke-determinisme er den centrale vanskelighed. Fejlen, der dukker op én kørsel af tusind, er hele problemet, ikke et kanttilfælde.
- Begræns alt. Ubegrænsede køer, trådantal og arbejde under udførelse forvandler en spids til et nedbrud.
- Modeller på højere niveau slår rå låse. Aktører, kanaler og struktureret samtidighed giver mange forfattere en sikker standard, og hver lås har en omkostning.
- Test interleavingerne, ikke kun den lykkelige sti. Deterministiske tests kan ikke fange en fejl, som kun en sjælden rækkefølge afslører.
Anbefalinger
Beslut, om du har brug for samtidighed eller parallelisme
Begynd med at navngive problemet. Hvis din tjeneste bruger det meste af sin tid på at vente (på databaser, netværkskald eller disk), har du en I/O-bundet arbejdsbyrde, og samtidighed er svaret: strukturér koden, så mens én anmodning venter, kan andre fortsætte. En enkelt tråd med async/await, eller en lille pulje, kan betjene tusindvis af ventende anmodninger. Hvis dit program i stedet er CPU-bundet og maler sig igennem beregning med lidt ventetid, så er det parallelisme på tværs af kerner, der køber hastighed, og her sættes loftet af Amdahls lov (se kapitel 2.16): den serielle andel begrænser din speedup uanset hvor mange kerner, du tilføjer. Mål, hvilket regime du er i, før du designer.
Behandl delt mutérbar tilstand som fjenden
Næsten hver samtidighedsfejl reducerer til samme form: to opgaver læser og skriver de samme foranderlige data uden at blive enige om en rækkefølge. Dette er en kapløbstilstand, og den producerer tabte opdateringer, halvskrevne objekter og værdier, der overtræder invarianter, koden antog var sikre. Det mest pålidelige forsvar er at have mindre delt mutérbar tilstand. Giv hver opgave sine egne data, send kopier frem for referencer, og begræns mutérbar tilstand til en enkelt ejer, som andre når gennem beskeder. Når du reelt må dele, så gør delingen eksplicit og lille, så en gennemganger kan se hvert sted, tilstanden røres.
Foretræk uforanderlighed og beskedudveksling som standarder
De sikreste delte data er data, der ikke kan ændres. Et uforanderligt objekt kan, når det først er konstrueret, læses af et vilkårligt antal tråde uden nogen synkronisering, fordi der intet er at konkurrere om. Gør uforanderlighed til din standard og mutérbarhed til den bevidste undtagelse. Når opgaver skal koordinere, så foretræk beskedudveksling frem for delt hukommelse: i stedet for at dele en fælles variabel lader du én opgave sende værdien til den anden, hvilket er filosofien bag Go-ordsproget “kommunikér ikke ved at dele hukommelse; del hukommelse ved at kommunikere.” Beskedudveksling forvandler usynlige, rækkefølgeafhængige fejl til eksplicit, inspicerbart dataflow, og den klarhed er næsten altid sin pris per besked værd i kode, mange vedligeholder.
Ræk ud efter modeller på højere niveau før rå låse
Håndskreven låsning er korrekt i princippet og katastrofal i praksis, fordi mennesker er dårlige til at ræsonnere om hver interleaving. Foretræk modeller, der gør sikker samtidighed til standarden. Aktørmodellen giver hver aktør privat tilstand og en postkasse: aktører deler aldrig hukommelse og sender kun beskeder, så hele klasser af kapløb forsvinder. Communicating sequential processes (CSP), modellen bag kanaler i sprog som Go, lader uafhængige processer sende værdier over typede kanaler. Struktureret samtidighed binder levetiden for samtidige opgaver til et leksikalsk scope, så opgaver ikke kan overleve den blok, der skabte dem, og fejl propageres i stedet for at forsvinde. Async/await lader dig skrive samtidig, I/O-bundet kode i en sekventiel stil. Hver af dem hæver gulvet for den gennemsnitlige forfatter, hvilket er, hvad et stort team har brug for.
Forstå din hukommelsesmodel, atomicitet og synlighed
Når du deler hukommelse, bider to egenskaber. Atomicitet betyder, at en operation sker helt eller slet ikke; en almindelig stigning (x = x + 1) er ikke atomisk, fordi den læser, adderer og skriver som tre trin, en anden tråd kan afbryde, hvilket er, hvordan tællere mister opdateringer. Synlighed betyder, at en skrivning fra én tråd bliver observerbar for en anden; uden korrekt synkronisering kan en værdi skrevet på én kerne sidde i en cache, en anden ikke ser, så en tråd kan løbe i løkke for evigt på et flag, der allerede var sat. Dit sprogs hukommelsesmodel definerer, hvornår skrivninger bliver synlige, og hvilke rækkefølger compileren og CPU’en må omarrangere, så du kan ikke antage, at kode kører i den rækkefølge, du skrev den. Brug sprogets atomiske typer og synkroniseringsprimitiver frem for at opfinde dit eget låsefrie skema.
Brug synkroniseringsprimitiver bevidst, og design mod deadlock
Når deling er uundgåelig, så ræk ud efter det rette primitiv og respektér dets pris. En lås eller mutex (mutual exclusion) lader én tråd ad gangen træde ind i en kritisk sektion, men serialiserer adgangen, så en varm lås bliver en flaskehals, der udsletter fordelen ved mange kerner. En semafor begrænser, hvor mange opgaver der må fortsætte på én gang, hvilket er, hvordan du begrænser en pulje. Atomiske operationer tilbyder låsefri opdateringer af simple værdier som tællere, billigere end en lås, men lette at misbruge til alt sammensat. Låse bringer tre klassiske fejlformer. En deadlock er, når opgaver venter på hinanden i en cyklus, og ingen kan fortsætte, med to tråde, der hver holder en lås og vil have den anden, som lærebogseksemplet. En livelock er, når opgaver bliver ved med at reagere på hinanden, men ikke gør fremskridt. Sult er, når en opgave aldrig får en ressource, fordi andre bliver ved med at springe foran. De discipliner, der forebygger disse, er konkrete: indfør en global låserækkefølge, hold låse kort, tilføj timeouts, så en fastlåst opgave fejler højlydt, kald aldrig ukendt kode, mens du holder en lås, og brug fair skedulering, hvor sult er en risiko. Skriv disse regler ned, for en ny forfatter kan ikke genopdage dem ud fra koden alene.
Begræns dine køer, puljer og arbejde under udførelse med modtryk
En ubegrænset kø er en tikkende bombe. Under en trafikspids ankommer arbejde hurtigere, end det tømmes, køen vokser uden grænse, hukommelsen fyldes, og tjenesten dør på en måde, der ligner et mystisk hukommelsesnedbrud frem for den overbelastning, det er. Begræns hver kø, sæt loft på hver trådpulje, og anvend modtryk: når systemet er fuldt, så signalér opstrøms at sænke tempoet eller afvis arbejde hurtigt frem for at acceptere uendeligt arbejde, du ikke kan nå. Dimensionér puljer efter arbejdsbyrden (omtrent kerneantallet for CPU-bundet arbejde, højere for I/O-bundet arbejde, hvor tråde mest venter), og behandl grænsen som en bevidst kapacitetsbeslutning. Det hænger sammen med robusthedsmønstrene i kapitel 3.3.
Brug dataparallelisme, hvor arbejdet er pinligt parallelt
Nogle problemer deler sig rent: anvend den samme operation på hvert element i et stort datasæt, uden at noget element afhænger af et andet. Denne dataparallelisme er den venligste slags, fordi der er lidt delt tilstand at konkurrere om, og speedup kan nærme sig kerneantallet, som map-reduce-pipelines, parallelle array-operationer og vektoriseret numerisk kode alle viser. Selv her skal du respektere Amdahls lov: flette- eller reduktionstrinnet er ofte seriel og begrænser din gevinst, og overheadet ved opdeling kan dominere for små input. Ræk ud efter den, når arbejdet pr. element er betydeligt, og elementerne er reelt uafhængige; ellers er den simpleste sekventielle version ofte både hurtig nok og langt lettere at holde korrekt, et punkt konstruktionspraksis i kapitel 2.9 forstærker.
Test og fejlsøg ikke-deterministisk kode med vilje
Samtidighedsfejl er ikke-deterministiske, så almindelige tests, som kører én interleaving, overser dem for det meste. Angrib problemet bevidst med stress- og fuzz-tests, der kører mange opgaver under randomiseret timing for at ryste sjældne rækkefølger ud. Ræk ud efter kapløbsdetektorer og tråd-sanitizere, værktøj der instrumenterer hukommelsesadgang for at fange datakapløb, selv når den fejlbehæftede interleaving ikke skete i denne kørsel. Hvor din platform tilbyder det, så brug deterministisk simulering eller kontrollerede skedulere, der afspiller en bestemt interleaving og forvandler en heisenbug til en reproducerbar, og design så et produktionshæng lader dig fange trådtilstande og låseejerskab, hvilket knytter sig til fejlsøgningsdisciplinen i kapitel 2.15. Frem for alt foretræk designs (uforanderlighed, beskedudveksling, enkelt ejerskab), der gør hele kategorier af disse fejl umulige, for en fejl, du ikke kan skabe, er en, du aldrig behøver at fejlsøge.
Afvejninger: fordele og ulemper
| Tilgang | Fordele | Ulemper |
|---|---|---|
| Delt hukommelse med låse | Hurtig pr. operation; velkendt | Kapløbs-, deadlock- og synlighedsfejl; svært for mange forfattere at holde korrekt |
| Uforanderlighed | Ingen synkronisering nødvendig; trivielt trådsikre læsninger | Kopieringsomkostning; akavet til store mutérbare strukturer |
| Beskedudveksling (aktører, kanaler) | Eksplicit dataflow; hele fejlklasser forsvinder | Overhead pr. besked; kan skjule modtryk hvis køer er ubegrænsede |
| Async/await | Billig samtidighed til I/O-bundet arbejde; sekventielt udseende kode | Ingen parallelisme til CPU-arbejde; blokering af en opgave sinker andre |
| Struktureret samtidighed | Klare opgavelevetider; fejl propageres; ingen lækkede opgaver | Nyere, mindre tilgængelig i nogle økosystemer |
| Dataparallelisme | Næsten lineær speedup på uafhængigt arbejde | Amdahl-loft; overhead dominerer små input |
| Atomics / låsefri | Ingen låsekonkurrence for simple værdier | Ekstremt let at få subtilt forkert; svær at gennemgå |
Den centrale spænding er sikkerhed versus rå hastighed, og løsningen er at købe korrekthed først og bruge ydeevne kun, hvor måling beviser, at du må. Rå låsning af delt hukommelse er den hurtigste pr. operation og den farligste pr. kodelinje; modeller på højere niveau koster lidt gennemløb og giver meget sikkerhed og klarhed tilbage, og for kode vedligeholdt af mange hænder er den handel afgjort umagen værd. Spar håndtunet låsefri samtidighed til de små hotspots, hvor en profiler (kapitel 2.16) beviser, at koordinationsoverhead betyder noget, og hold selv dem bag en veltestet grænse.
Spørgsmål til drøftelse med dit team
For jeres travleste tjeneste, er arbejdsbyrden I/O-bundet eller CPU-bundet, og matcher jeres samtidighedsdesign? Teams tilføjer rutinemæssigt trådpuljer til tjenester, der bruger 95 % af tiden på at vente på en database, hvilket giver konkurrence, men intet gennemløb, eller de forsøger at parallelisere en beregning, hvis serielle andel begrænser enhver speedup. Det rette design følger af regimet: async eller en lille pulje til ventetungt arbejde, ægte parallelisme på tværs af kerner til beregningstungt arbejde. Tag en profil med, der viser, hvor tiden faktisk går hen, ikke en antagelse, og hvis det meste af tiden bruges på at beregne, så mål den serielle andel og lad Amdahls lov fortælle jer loftet. Svaret former, om I griber til async, en begrænset pulje eller dataparallelisme.
Hvad er jeres teams standard for at dele tilstand mellem opgaver, og er den sikker ved konstruktion? I et stort team betyder standarden mere end undtagelserne, fordi det meste kode skrives af folk, der ikke er samtidighedsspecialister, og som kopierer det mønster, der allerede er der. Hvis standarden er delte mutérbare objekter vogtet af ad hoc-låse, er I én glemt lås fra et kapløb, der dukker op måneder senere i produktion. Hvis standarden er uforanderlighed og beskedudveksling, opstår hele kategorier af fejl aldrig, og det sjældne sted, der reelt har brug for delt hukommelse, træder frem til omhyggelig gennemgang. Drøft, hvad en ny ingeniør ville række ud efter i dag, om jeres gennemgange ville fange en usynkroniseret skrivning, og hvordan I gør den sikre sti til den lette.
Hvordan ville I finde, reproducere og rette en samtidighedsfejl, der optræder én gang pr. million anmodninger i produktion? Det ærlige svar for mange teams er, at de ikke kunne, fordi fejlen forsvinder, når de kigger, og deres tests kun nogensinde kører én godartet interleaving. Det bør bekymre jer, fordi disse fejl stille korrumperer data og udhuler tillid. Tal om, hvorvidt I kører kapløbsdetektorer og tråd-sanitizere i continuous integration, om I stresstester med randomiseret timing, og om jeres produktionsobserverbarhed fanger tråd- og låsetilstand i det øjeblik, et hæng opstår. De bedste teams svarer ved at gøre de fleste sådanne fejl umulige gennem deres valg af model, så de få resterende er sjældne og indeholdte.
Hvor i jeres system findes der stadig en ubegrænset kø eller en ucappet trådpulje, og hvad sker der med den under en pludselig tidobbelt spids? Det betyder noget, fordi ubegrænset arbejde under udførelse er den fejl, der klæder sig ud som et mystisk hukommelsesnedbrud: arbejde ankommer hurtigere, end det tømmes, hukommelsen fyldes, og tjenesten dør og ligner en hardwarefejl frem for den overbelastning, det er. De modstridende hensyn er reelle, fordi en grænse sat for lavt afviser legitim trafik, og en grænse sat for højt udskyder nedbruddet i stedet for at forebygge det, så tallet er en kapacitetsbeslutning, ikke et gæt. Tag en opgørelse med over hver kø og pulje, dens nuværende grænse (eller indrømmelsen af, at den ingen har), modtryksadfærden, når den fyldes, og belastningstestdokumentation for, hvordan systemet nedbrydes ved kanten. For en virksomhedsflåde kan en enkelt ubegrænset kø kaskadere til et flådedækkende nedbrud, og for en offentlig platform, der skal forblive tilgængelig for borgere, er elegant afvisning med en klar fejl en serviceforpligtelse, så grænsen og dens afvisningssti hører hjemme i kapacitetsplanen og runbooken, ikke i én ingeniørs hukommelse.
Hvad er jeres teams politik for at bruge samtidighedsmodeller på højere niveau versus håndskrevne låse, og hvor har I tilladt undtagelser? Standardmodellen afgør, hvor sikker den gennemsnitlige ændring er, fordi de fleste forfattere ikke er samtidighedsspecialister og vil kopiere det mønster, der allerede findes: aktører, kanaler og struktureret samtidighed hæver gulvet for alle, mens rå låsning er korrekt i teorien og en kilde til deadlocks i praksis. Spændingen er, at modeller på højere niveau koster lidt overhead pr. besked eller opgave, og en profiler vil lejlighedsvis bevise, at en varm sti har brug for håndtunet låsefri kode, så et generelt forbud er lige så forkert som et frit-for-alle. Tag listen med over steder, hvor I er gået under den sikre standard, den profileringsdokumentation, der retfærdiggjorde hver, og hvordan hver undtagelse er indhegnet bag en testet grænse og en dokumenteret låserækkefølge. I en stor virksomhed er denne politik det, der forhindrer tusindvis af bidragydere i hver at genopfinde et usikkert skema, og i et langlivet offentligt system er det, der lader en gennemganger år senere forstå, hvorfor et farligt mønster blev tilladt, og bekræfte at det stadig er berettiget.
Når I beslutter at parallelisere en beregning, hvordan måler I den serielle andel, og hvem er ansvarlig for at bekræfte, at speedup er reel? Teams spreder rutinemæssigt en beregning over kerner og fejrer et tal, en profiler aldrig ville bekræfte, fordi Amdahls lov begrænser gevinsten til den reciprokke af den serielle andel uanset hvor mange kerner, du tilføjer, og opdelings- og fletteoverhead kan udslette fordelen helt for små input. Det modstridende træk er, at parallelisme tilføjer reel kompleksitet og ny kapløbsflade, så spørgsmålet er, om den målte speedup retfærdiggør den korrekthedsrisiko, I tager på jer. Tag en profil med, der isolerer den serielle del, de inputstørrelser hvor parallelisme faktisk vinder, og et før-og-efter-benchmark på repræsentativ hardware frem for et håbefuldt skøn. For en virksomhed, der betaler for en stor beregningsflåde, bliver en ærlig analyse af den serielle andel til sparet eller spildt hardwareudgift, og for et offentligt organ, der skal stå til regnskab for omkostningen ved et offentligt system, bør den person, der godkendte det parallelle design, kunne vise den måling, der retfærdiggjorde det, under revision.
Sektorperspektiv
Startup. Med et lille team og ingen runway at give af så køb korrekthed med struktur, ikke med en samtidighedsspecialist, du ikke kan ansætte. Ræk ud efter den ene sikre standard, dit sprog giver, async/await til I/O-bundet arbejde, én ejende opgave eller en aktør til enhver delt tilstand, og spring håndtunet låsning helt over. Et tabt-opdatering-kapløb i en betalingssti kan sænke dig hurtigere end en misset feature, så brug den lille mængde ekstra kode på at gøre den klasse af fejl umulig og gå videre.
Lille virksomhed. Du har ingen, hvis job er samtidighed, så foretræk platforme og administrerede tjenester, der håndterer det for dig: en databasetransaktion, en hostet kø eller et frameworks anmodningsmodel slår tråde, du vedligeholder i hånden. Når du evaluerer et værktøj, så behandl “gør dette samtidighed sikker som standard” som et køb-versus-byg-spørgsmål, og foretræk muligheden, hvor en forkert interleaving ikke stille kan korrumpere en kundes post. Hold delt mutérbar tilstand ude af din egen kode, hvor en begrænset, administreret tjeneste kan holde den i stedet.
Stor virksomhed. På tværs af mange teams er målet en husstandard, der holder tusindvis af bidragydere sikre: uforanderlighed og beskedudveksling som normen, modeller på højere niveau frem for rå låse, begrænsede køer og puljer med modtryk og en dokumenteret global låserækkefølge. Kod dem ind i ingeniørstandarder, håndhæv dem med kapløbsdetektorer og stresstests i CI, og styr undtagelserne, hvor en profiler retfærdiggjorde låsefri kode, så hver forbliver bag en testet, gennemgået grænse. Styr samtidighedskapacitet som et flådedækkende anliggende med kø-grænser og puljestørrelser bundet til målt belastning.
Offentlig sektor. Korrekthed og reviderbarhed i systemer, der kører i årtier, vejer tungere end rå gennemløb. Kræv, at hver tilstandsovergang registreres og kan afspilles igen, så et mistænkt kapløb kan reproduceres, og rettelsen bevises over for et tilsynsorgan, og hold deterministiske stier fri for AI til beslutninger, der rører ydelser, sikkerhed eller offentlige registre. Indkøb bør kræve, at leverandører oplyser deres samtidighedsmodel og dokumentation for dækning med kapløbsdetektor og stresstest, fordi et forkert svar under belastning i et offentligt system ikke er en ulejlighed, men noget en ansvarlig embedsmand senere må forklare.
Eksempler
Startup. Et lille team udgiver en betalingsfeature og bemærker, at kontosaldi lejlighedsvis driver nogle få øre under belastning. Årsagen er en almindelig læs-ændr-skriv på et saldofelt fra samtidige anmodningshåndterere, et tabt-opdatering-kapløb. I stedet for at drysse låse flytter de hver kontos saldo bag en enkelt ejende opgave, der behandler debiteringer og krediteringer som beskeder, én ad gangen. Driften forsvinder, koden bliver let at ræsonnere om, og de tilføjer en stresstest, der affyrer tusindvis af samtidige overførsler for at vogte rettelsen. Én strukturel ændring, en hel klasse af fejl afviklet.
Stor virksomhed. En ordretjeneste med højt gennemløb, der håndterer titusindvis af anmodninger i sekundet, lider under periodiske latensspidser og lejlighedsvise hukommelsesnedbrud under trafikbølger. Undersøgelsen finder en ubegrænset arbejdskø bag en trådpulje, der vokser uden grænse, når efterspørgslen overstiger kapaciteten. Teamet begrænser køen, sætter loft på puljen til en størrelse bundet til kerneantallet og tilføjer modtryk, der afviser overskydende belastning hurtigt med en klar fejl. Gennemløbet bliver forudsigeligt, nedbruddene stopper, og en varm lås på en fælles cache erstattes af en låsefri struktur først, efter en profiler beviser, at konkurrencen er reel. Sikre standarder til de mange forfattere, tunet samtidighed kun, hvor det er målt.
Offentlig sektor. En national ydelsesplatform kører i årtier og skal producere reviderbare, korrekte resultater selv under samtidige sagsopdateringer. Teamet vælger uforanderlighed og beskedudveksling som husstandard, begrænser hver stump mutérbar tilstand til en enkelt ejer og indfører en global låserækkefølge, hvor der stadig er låse, alt sammen skrevet ind i ingeniørstandarderne. De kører tråd-sanitizere og randomiserede stresstests i pipelinen og designer, så hver tilstandsovergang registreres og kan afspilles igen for tilsyn, hvilket lader dem reproducere og bevise rettelsen, når en sjælden interleaving mistænkes. Korrekthed og reviderbarhed behandles som førsteklasses krav, ikke ydeevne-eftertanker.
Business case: motivation, ROI og TCO
Afkastet af disciplineret samtidighed viser sig som hændelser, der aldrig sker. Et enkelt produktionskapløb kan korrumpere data på tværs af tusindvis af poster, og omkostningen omfatter både ingeniørtimerne til at finde en fejl, der skjuler sig, når den observeres, og den langt større omkostning ved at afstemme dårlige data, underrette berørte brugere og genopbygge tillid. Det er blandt de dyreste fejl at diagnosticere netop fordi de er ikke-deterministiske, så at jage én heisenbug kan overstige indsatsen ved at vælge en sikker model på forhånd.
Gevinsten viser sig også som gennemløb og omkostning. At dimensionere samtidighed rigtigt lader en tjeneste håndtere langt mere belastning på den samme hardware, en tilbagevendende besparelse for en stor flåde, mens modtryk og begrænsede køer forebygger de kaskaderende nedbrud, der forvandler en trafikspids til en offentlig hændelse. De totale ejeromkostninger er beskedne og mest kulturelle: I investerer i en husstil (uforanderlighed, beskedudveksling, struktureret samtidighed), i værktøj (kapløbsdetektorer, tråd-sanitizere, stressharnesses i CI) og i standarder, der koder låserækkefølge og begrænsning. Alternativet er en kodebase, hvor korrekthed afhænger af, at hver forfatter er ekspert for evigt, hvilket intet voksende team kan opretholde. Argumentér over for ledelsen i deres enheder: oversæt et forebygget kapløb til undgåede datakorruptionshændelser, modtryk til forebyggede nedbrud og en sikker standard til sparet onboarding-tid.
Anti-mønstre og faldgruber
- At tilføje tråde for hastighed på I/O-bundet arbejde. Flere tråde på en ventetung tjeneste køber konkurrence, ikke gennemløb.
- Delt mutérbar tilstand overalt. Enhver tråd, der muterer ethvert objekt, gør korrekthed til et spørgsmål om held, ingen gennemganger kan verificere.
- Ubegrænsede køer og puljer. En spids vokser køen, til hukommelsen dør; nedbruddet ser mystisk ud, men er almindelig overbelastning.
- Ad hoc-låsning uden global rækkefølge. Låse taget i forskellig rækkefølge i kodebasen deadlocker under belastning.
- At antage at kode kører i skrevet rækkefølge. At ignorere hukommelsesmodellen, så en synlighedsfejl lader en tråd spinne på en forældet værdi.
- Håndrullet låsefri snilde. Skræddersyede låsefri skemaer er næsten altid subtilt forkerte og næsten umulige at gennemgå.
- Kun at teste den lykkelige interleaving. Deterministiske tests består, mens den ene-i-en-million rækkefølge korrumperer produktion.
- At kalde ukendt kode, mens man holder en lås. Et callback, der blokerer eller går ind igen, forvandler en kritisk sektion til en deadlock.
Modenhedsmodel
- Niveau 1, Opstart: Samtidighed er ad hoc og reaktiv. Tråde og låse tilføjes på instinkt, delt mutérbar tilstand er overalt, og køer er ubegrænsede. Kapløbstilstande dukker op som ureproducerbare produktionshændelser, ingen kan diagnosticere, og der findes intet værktøj til at fange dem.
- Niveau 2, Udvikling: Nogle teams har lært grundlæggende praksis: de bruger låse med mere omhu og begrænser deres mest åbenlyse køer. Der er uformel bevidsthed om kapløb og deadlocks, og nogle få kritiske stier får ekstra granskning. Praksis er inkonsistent på tværs af teams, test er stadig mest enkelt-interleaving, og sikre mønstre bor hos enkeltpersoner frem for på skrift.
- Niveau 3, Standardisering: Organisationen har en dokumenteret husstil håndhævet i hele organisationen: uforanderlighed og beskedudveksling som standarder, modeller på højere niveau frem for rå låse, begrænsede køer og puljer med modtryk og en dokumenteret global låserækkefølge. Kapløbsdetektorer og stresstests kører i CI, og samtidighedsvalg følger af, om arbejdet er I/O-bundet eller CPU-bundet.
- Niveau 4, Styring: Organisationen måler og styrer sin samtidighedsholdning mod baselines. Den følger dækning af kapløbsdetektor og tråd-sanitizer på tværs af tjenester, registrerer kødybde, låseventetid, puljemætning og afvisningsrater som overvågede målinger og belastningstester nedbrydningskurven, så hver grænse er en databaseret kapacitetsbeslutning. Samtidighedshændelser tælles og tendensanalyseres, serielle andele af parallelliserede arbejdsbyrder måles mod den speedup, der faktisk opnås, og go/no-go-beslutninger om nye designs hviler på den dokumentation frem for på instinkt.
- Niveau 5, Orkestrering: Sikker samtidighed er den sti, der møder mindst modstand, for hver forfatter, og praksis forbedres løbende og er integreret i hele organisationen. Hele fejlklasser er umulige ved konstruktion, hotspots tunes kun, hvor profilering beviser det, og deterministisk afspilning gør den sjældne resterende fejl reproducerbar. Korrekthed og reviderbarhed er løbende forsvarede egenskaber, kapacitetsgrænser tilpasser sig observeret belastning, og standarderne udvikler sig, efterhånden som platformen og arbejdsbyrden skifter.
Idéer til drøftelse
- Hvis I reviderede jeres travleste tjeneste i dag, hvor meget af dens tilstand er delt og mutérbar, og hvor meget af den deling er reelt nødvendig?
- Hvad er jeres teams standardsvar, når nogen har brug for, at to opgaver koordinerer, og ville I hellere have, at det var uforanderlighed eller beskedudveksling?
- Hvor gemmer ubegrænsede køer eller ucappede puljer sig stadig i jeres system, og hvad ville der ske med dem under en pludselig tidobbelt trafikspids?
- Inkluderer jeres continuous integration-kørsler en kapløbsdetektor eller tråd-sanitizer, og hvornår fangede en sidst noget før produktion?
- For jeres mest parallelliserede arbejdsbyrde, hvad er den serielle andel, og sætter Amdahls lov et loft over den speedup, I faktisk jagter?
- Kunne jeres team reproducere en ene-i-en-million interleaving-fejl efter behov, og hvad ville der skulle til for at komme dertil?
Vigtigste pointer
- Samtidighed strukturerer et program som uafhængige opgaver; parallelisme eksekverer dem på én gang. Beslut, hvilken du har brug for, før du tilføjer tråde.
- Delt mutérbar tilstand er roden til næsten hver samtidighedsfejl; foretræk uforanderlighed og beskedudveksling som sikre standarder for mange forfattere.
- Ræk ud efter modeller på højere niveau (aktører, kanaler, struktureret samtidighed, async/await) før håndskrevne låse, som er korrekte i teorien og farlige i praksis.
- Forstå atomicitet, synlighed og din hukommelsesmodel; brug det rette primitiv, hold låse kort, og indfør en global låserækkefølge for at undgå deadlock, livelock og sult.
- Begræns hver kø og pulje og anvend modtryk, så en spids nedbrydes elegant frem for at crashe (kapitel 3.3).
- Test interleavingerne med vilje med kapløbsdetektorer, stresstests og afspilning (kapitel 2.15), og respektér Amdahls lov, når du parallelliserer (kapitel 2.16).
- For virksomheder er dette gennemløb og forebyggede hændelser; for det offentlige er det korrekthed og reviderbarhed i langlivede systemer.
Referencer og videre læsning
- 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.