2.17

View in English

2.17 Samtidighet och parallellism

Översikt och motivation

Samtidighet är konsten att strukturera ett program som oberoende uppgifter som kan göra framsteg utan att vänta på varandra. Parallellism är att faktiskt köra de uppgifterna i samma ögonblick på flera processorer. Skillnaden är inte pedantisk. Samtidighet är ett sätt att organisera kod så att ett långsamt nätverksanrop inte fryser hela programmet. Parallellism är ett sätt att slutföra en stor beräkning snabbare genom att sprida den över kärnor. Att förväxla de två leder team att lägga till trådar i hopp om hastighet och få bara buggar.

För stora team spelar detta ämne roll eftersom samtidighet är där korrekthet tyst dör. En enskild författare som skriver entrådad kod kan resonera om den rad för rad, men i samma stund många författare delar minne över trådar exploderar antalet möjliga sammanflätningar, och ett program som klarar varje test kan ändå fallera en gång på en miljon under produktionsbelastning, inte med en högljudd krasch utan med korrumperade data, hängande begäranden och incidenter ingen kan reproducera. Det här kapitlet bygger på kodnivåfokuset i kapitel 2.16 (prestandateknik) och datavetenskapens grunder i kapitel 2.13, och det matar in i samordningsproblemen i kapitel 3.3 (distribuerade system), som är samtidighet över maskiner med nätverkets opålitlighets extra grymhet.

För företag är samtidighetsbuggar genomströmningsbuggar. Högtrafikerade tjänster lever eller dör på sin förmåga att hantera tusentals samtidiga begäranden utan att tävla om delat tillstånd, och en enda osynkroniserad räknare kan korrumpera en huvudbok under belastning. För myndigheter är insatserna korrekthet och granskningsbarhet i system som körs i decennier och rör säkerhet, bidrag eller offentliga handlingar. Ett kapplöpningstillstånd i ett skatte- eller hälsosystem är ingen olägenhet. Det är ett felaktigt svar som någon senare måste förklara för ett tillsynsorgan. I båda miljöerna är målet detsamma: gör den säkra vägen till standard, så att de många som rör koden inte alla behöver vara samtidighetsexperter.

Nyckelprinciper

  • Samtidighet är struktur. Parallellism är körning. Besluta vilken du faktiskt behöver innan du sträcker dig efter trådar.
  • Delat föränderligt tillstånd är fienden. Nästan varje samtidighetsbugg kan spåras till två uppgifter som rör samma föränderliga data.
  • Föredra oföränderlighet och meddelandeförmedling. Data som inte kan ändras kan inte tävlas om, och meddelanden slår delat minne för säkerhet.
  • Icke-determinism är den centrala svårigheten. Buggen som dyker upp en körning på tusen är hela problemet, inte ett kantfall.
  • Begränsa allt. Obegränsade köer, trådantal och arbete under utförande förvandlar en topp till ett avbrott.
  • Modeller på högre nivå slår råa lås. Aktörer, kanaler och strukturerad samtidighet ger många författare ett säkert standardval, och varje lås har en kostnad.
  • Testa sammanflätningarna, inte bara den lyckade vägen. Deterministiska tester kan inte fånga en bugg som bara en sällsynt ordning avslöjar.

Rekommendationer

Besluta om du behöver samtidighet eller parallellism

Börja med att namnge problemet. Om din tjänst spenderar det mesta av sin tid på att vänta (på databaser, nätverksanrop eller disk) har du en I/O-bunden arbetsbelastning, och samtidighet är svaret: strukturera koden så att medan en begäran väntar fortsätter andra. En enda tråd med async/await, eller en liten pool, kan betjäna tusentals väntande begäranden. Om ditt program i stället är CPU-bundet, och maler genom beräkning med lite väntan, är det parallellism över kärnor som köper hastighet, och här sätts taket av Amdahls lag (se kapitel 2.16): den seriella andelen begränsar din uppsnabbning oavsett hur många kärnor du lägger till. Mät vilket regime du befinner dig i innan du designar.

Behandla delat föränderligt tillstånd som fienden

Nästan varje samtidighetsdefekt reduceras till samma form: två uppgifter läser och skriver samma föränderliga data utan att vara överens om en ordning. Det är ett kapplöpningstillstånd, och det ger förlorade uppdateringar, halvskrivna objekt och värden som bryter mot invarianter koden antog var säkra. Det mest pålitliga försvaret är att ha mindre delat föränderligt tillstånd. Ge varje uppgift sina egna data, skicka kopior snarare än referenser och begränsa föränderligt tillstånd till en enda ägare som andra når genom meddelanden. När du verkligen måste dela, gör delningen uttrycklig och liten, så att en granskare kan se varje ställe tillståndet rörs.

Föredra oföränderlighet och meddelandeförmedling som standardval

De säkraste delade data är data som inte kan ändras. Ett oföränderligt objekt kan, när det väl konstruerats, läsas av valfritt antal trådar utan någon synkronisering, eftersom det inte finns något att tävla om. Gör oföränderlighet till ditt standardval och föränderlighet till det medvetna undantaget. När uppgifter måste samordnas, föredra meddelandeförmedling framför delat minne: i stället för att dela en gemensam variabel, låt en uppgift skicka värdet till den andra, vilket är filosofin bakom Go-ordspråket “kommunicera inte genom att dela minne, dela minne genom att kommunicera”. Meddelandeförmedling förvandlar osynliga, ordningsberoende buggar till uttryckligt, inspekterbart dataflöde, och den tydligheten är nästan alltid värd sin kostnad per meddelande i kod som många underhåller.

Sträck dig efter modeller på högre nivå före råa lås

Handskriven låsning är korrekt i princip och katastrofal i praktiken, eftersom människor är dåliga på att resonera om varje sammanflätning. Föredra modeller som gör säker samtidighet till standard. Aktörsmodellen ger varje aktör privat tillstånd och en brevlåda: aktörer delar aldrig minne och skickar bara meddelanden, så hela klasser av kapplöpningar försvinner. Kommunicerande sekventiella processer (CSP), modellen bakom kanaler i språk som Go, låter oberoende processer skicka värden över typade kanaler. Strukturerad samtidighet knyter livslängden hos samtidiga uppgifter till en lexikal omfattning, så att uppgifter inte kan överleva blocket som startade dem och fel propagerar i stället för att försvinna. Async/await låter dig skriva samtidig, I/O-bunden kod i sekventiell stil. Var och en av dessa höjer golvet för den genomsnittlige författaren, vilket är vad ett stort team behöver.

Förstå din minnesmodell, atomicitet och synlighet

När du delar minne är det två egenskaper som bits. Atomicitet betyder att en operation sker allt på en gång eller inte alls. Ett vanligt inkrement (x = x + 1) är inte atomärt, eftersom det läser, adderar och skriver som tre steg en annan tråd kan avbryta, vilket är hur räknare förlorar uppdateringar. Synlighet betyder att en skrivning av en tråd blir observerbar för en annan. Utan korrekt synkronisering kan ett värde skrivet på en kärna ligga i en cache osynligt för en annan, så en tråd kan loopa för evigt på en flagga som redan var satt. Ditt språks minnesmodell definierar när skrivningar blir synliga och vilka ordningar kompilatorn och CPU:n får ordna om, så du kan inte anta att kod körs i den ordning du skrev den. Använd språkets atomära typer och synkroniseringsprimitiver i stället för att uppfinna ditt eget låsfria schema.

Använd synkroniseringsprimitiver medvetet och designa mot baklås

När delning är oundviklig, sträck dig efter rätt primitiv och respektera dess pris. Ett lås eller en mutex (ömsesidig uteslutning) låter en tråd i taget gå in i ett kritiskt avsnitt, men serialiserar åtkomst, så ett hett lås blir en flaskhals som utplånar nyttan av många kärnor. En semafor begränsar hur många uppgifter som får fortsätta samtidigt, vilket är hur du begränsar en pool. Atomära operationer erbjuder låsfria uppdateringar för enkla värden som räknare, billigare än ett lås men lätta att missbruka för allt sammansatt. Lås för med sig tre klassiska felmönster. Ett baklås är när uppgifter väntar på varandra i en cykel och ingen kan fortsätta, lärobokexemplet är två trådar som var och en håller ett lås och vill ha det andra. Ett livelock är när uppgifter fortsätter reagera på varandra men inte gör några framsteg. Svält är när en uppgift aldrig får en resurs eftersom andra hela tiden går före. De discipliner som förebygger dessa är konkreta: inför en global låsordning, håll lås kort, lägg till tidsgränser så att en fastnad uppgift fallerar högljutt, anropa aldrig okänd kod medan du håller ett lås och använd rättvis schemaläggning där svält är en risk. Skriv ner dessa regler, för en ny författare kan inte återupptäcka dem enbart ur koden.

Begränsa dina köer, pooler och arbete under utförande med mottryck

En obegränsad kö är en tidsinställd bomb. Under en trafiktopp anländer arbete snabbare än det töms, kön växer utan gräns, minnet fylls och tjänsten dör på ett sätt som ser ut som en mystisk krasch av slut på minne snarare än den överbelastning det är. Begränsa varje kö, sätt tak på varje trådpool och tillämpa mottryck: när systemet är fullt, signalera uppströms att sakta ner eller avvisa arbete snabbt i stället för att acceptera oändligt arbete du inte kan slutföra. Dimensionera pooler efter arbetsbelastningen (ungefär kärnantalet för CPU-bundet arbete, högre för I/O-bundet arbete där trådar mest väntar) och behandla gränsen som ett medvetet kapacitetsbeslut. Det hänger ihop med motståndskraftsmönstren i kapitel 3.3.

Använd dataparallellism där arbetet är pinsamt parallellt

Vissa problem delar sig rent: tillämpa samma operation på varje element i en stor datamängd, utan att något element beror på ett annat. Denna dataparallellism är den vänligaste sorten, eftersom det finns lite delat tillstånd att tävla om och uppsnabbningen kan närma sig kärnantalet, som map-reduce-pipelines, parallella arrayoperationer och vektoriserad numerisk kod alla visar. Även här, respektera Amdahls lag: sammanslagnings- eller reduceringssteget är ofta seriellt och begränsar din vinst, och overheaden av uppdelningen kan dominera för små indata. Sträck dig efter det när arbetet per element är betydande och elementen verkligen är oberoende. Annars är den enklaste sekventiella versionen ofta både tillräckligt snabb och långt lättare att hålla korrekt, en poäng konstruktionspraxis i kapitel 2.9 förstärker.

Testa och felsök icke-deterministisk kod med avsikt

Samtidighetsbuggar är icke-deterministiska, så vanliga tester, som kör en sammanflätning, missar dem mest. Angrip problemet medvetet med stress- och fuzztester som kör många uppgifter under slumpmässig timing för att skaka fram sällsynta ordningar. Sträck dig efter kapplöpningsdetektorer och trådsanerare, verktyg som instrumenterar minnesåtkomst för att fånga datakapplöpningar även när den felaktiga sammanflätningen inte inträffade den här körningen. Där din plattform erbjuder det, använd deterministisk simulering eller kontrollerade schemaläggare som spelar upp en specifik sammanflätning och förvandlar en heisenbugg till en reproducerbar, och designa så att ett produktionshäng låter dig fånga trådtillstånd och låsägarskap, vilket knyter an till felsökningsdisciplinen i kapitel 2.15. Framför allt, föredra designer (oföränderlighet, meddelandeförmedling, enskilt ägarskap) som gör hela kategorier av dessa buggar omöjliga, för en bugg du inte kan skapa är en du aldrig behöver felsöka.

Avvägningar: för- och nackdelar

TillvägagångssättFördelarNackdelar
Delat minne med låsSnabbt per operation. VälbekantKapplöpnings-, baklås- och synlighetsbuggar. Svårt för många författare att hålla korrekt
OföränderlighetIngen synkronisering behövs. Trivialt trådsäkra läsningarKopieringskostnad. Krångligt för stora föränderliga strukturer
Meddelandeförmedling (aktörer, kanaler)Uttryckligt dataflöde. Hela buggklasser försvinnerOverhead per meddelande. Kan dölja mottryck om köer är obegränsade
Async/awaitBillig samtidighet för I/O-bundet arbete. Sekventiellt utseende kodIngen parallellism för CPU-arbete. Att blockera en uppgift stoppar andra
Strukturerad samtidighetTydliga uppgiftslivslängder. Fel propagerar. Inga läckta uppgifterNyare, mindre tillgängligt i vissa ekosystem
DataparallellismNästan linjär uppsnabbning på oberoende arbeteAmdahl-tak. Overhead dominerar små indata
Atomära / låsfriaIngen låskonkurrens för enkla värdenExtremt lätt att få subtilt fel. Svårt att granska

Den centrala spänningen är säkerhet mot rå hastighet, och lösningen är att köpa korrekthet först och spendera prestanda bara där mätning bevisar att du måste. Rå delat-minne-låsning är snabbast per operation och farligast per kodrad. Modeller på högre nivå kostar lite genomströmning och ger tillbaka mycket säkerhet och tydlighet, och för kod som underhålls av många händer är det bytet avgjort värt att göra. Reservera handjusterad låsfri samtidighet för de små heta punkter där en profilerare (kapitel 2.16) bevisar att samordningsoverheaden spelar roll, och håll även dessa bakom en väl testad gräns.

Frågor att diskutera med ditt team

  1. För er mest trafikerade tjänst, är arbetsbelastningen I/O-bunden eller CPU-bunden, och matchar er samtidighetsdesign det? Team lägger rutinmässigt till trådpooler till tjänster som spenderar 95 % av sin tid på att vänta på en databas, och får konkurrens men ingen genomströmning, eller så försöker de parallellisera en beräkning vars seriella andel begränsar varje uppsnabbning. Rätt design följer av regimen: async eller en liten pool för väntetungt arbete, verklig parallellism över kärnor för beräkningstungt arbete. Ta med en profil som visar var tiden faktiskt går, inte ett antagande, och om mest tid spenderas på beräkning, mät den seriella andelen och låt Amdahls lag tala om taket. Svaret formar om ni sträcker er efter async, en begränsad pool eller dataparallellism.

  2. Vilket är ert teams standardval för att dela tillstånd mellan uppgifter, och är det säkert per konstruktion? I ett stort team spelar standardvalet större roll än undantagen, eftersom det mesta av koden skrivs av människor som inte är samtidighetsspecialister och som kopierar vilket mönster som redan finns. Om standardvalet är delade föränderliga objekt vaktade av ad hoc-lås är ni ett glömt lås från ett kapplöpningstillstånd som dyker upp månader senare i produktion. Om standardvalet är oföränderlighet och meddelandeförmedling inträffar hela kategorier av buggar aldrig, och det sällsynta ställe som verkligen behöver delat minne sticker ut för noggrann granskning. Diskutera vad en ny ingenjör skulle sträcka sig efter i dag, om era granskningar skulle fånga en osynkroniserad skrivning och hur ni gör den säkra vägen till den enkla.

  3. Hur skulle ni hitta, reproducera och åtgärda en samtidighetsbugg som dyker upp en gång på en miljon begäranden i produktion? Det ärliga svaret för många team är att de inte kunde, eftersom buggen försvinner när de tittar och deras tester bara någonsin kör en ofarlig sammanflätning. Det borde oroa er, eftersom dessa buggar tyst korrumperar data och urholkar förtroende. Prata om huruvida ni kör kapplöpningsdetektorer och trådsanerare i kontinuerlig integration, om ni stresstestar med slumpmässig timing och om er produktionsobserverbarhet fångar tråd- och låstillstånd i ögonblicket för ett häng. De bästa teamen svarar genom att göra de flesta sådana buggar omöjliga genom sitt modellval, så att de få som återstår är sällsynta och avgränsade.

  4. Var i ert system finns fortfarande en obegränsad kö eller en pool utan tak, och vad händer med den under en plötslig tiofaldig topp? Det här spelar roll eftersom obegränsat arbete under utförande är felet som maskerar sig som en mystisk krasch av slut på minne: arbete anländer snabbare än det töms, minnet fylls och tjänsten dör och ser ut som ett hårdvarufel snarare än den överbelastning det är. De motstridiga hänsynen är verkliga, eftersom en gräns ni sätter för lågt avvisar legitim trafik och en gräns för högt skjuter upp kraschen i stället för att förhindra den, så talet är ett kapacitetsbeslut, inte en gissning. Ta med en inventering av varje kö och pool, dess nuvarande gräns (eller erkännandet att den saknar en), mottrycksbeteendet när den fylls och belastningstestbelägg för hur systemet degraderar vid kanten. För en företagsflotta kan en enda obegränsad kö kaskadera till ett flottomfattande avbrott, och för en myndighetsplattform som måste förbli tillgänglig för medborgare är graciöst avvisande med ett tydligt fel en tjänsteskyldighet, så gränsen och dess avvisningsväg hör hemma i kapacitetsplanen och körboken, inte i en ingenjörs minne.

  5. Vilken är er policy för att använda samtidighetsmodeller på högre nivå mot handskrivna lås, och var har ni tillåtit undantag? Standardmodellen avgör hur säker den genomsnittliga ändringen är, eftersom de flesta författare inte är samtidighetsspecialister och kommer att kopiera vilket mönster som redan finns: aktörer, kanaler och strukturerad samtidighet höjer golvet för alla, medan rå låsning är korrekt i teorin och en källa till baklås i praktiken. Spänningen är att modeller på högre nivå kostar lite overhead per meddelande eller uppgift, och en profilerare kommer ibland att bevisa att en het väg behöver handjusterad låsfri kod, så ett generellt förbud är lika fel som en fri-för-alla. Ta med listan över ställen där ni har gått under det säkra standardvalet, profileringsbeläggen som motiverade var och en och hur varje undantag är avgränsat bakom en testad gräns och en dokumenterad låsordning. I ett stort företag är den här policyn det som hindrar tusentals bidragsgivare från att var och en uppfinna ett osäkert schema, och i ett långlivat myndighetssystem är det vad som låter en granskare år senare förstå varför ett farligt mönster tilläts och bekräfta att det fortfarande är motiverat.

  6. När ni beslutar att parallellisera en beräkning, hur mäter ni den seriella andelen, och vem är ansvarig för att bekräfta att uppsnabbningen är verklig? Team sprider rutinmässigt en beräkning över kärnor och firar ett tal som en profilerare aldrig skulle bekräfta, eftersom Amdahls lag begränsar vinsten till den seriella andelens reciproka värde oavsett hur många kärnor du lägger till, och overheaden av uppdelning och sammanslagning kan utplåna nyttan helt för små indata. Det motstridiga draget är att parallellism lägger till verklig komplexitet och ny yta för kapplöpningar, så frågan är om den uppmätta uppsnabbningen motiverar den korrekthetsrisk ni tar på er. Ta med en profil som isolerar den seriella delen, de indatastorlekar där parallellism faktiskt vinner och ett före-och-efter-riktmärke på representativ hårdvara snarare än en hoppfull uppskattning. För ett företag som betalar för en stor beräkningsflotta blir en ärlig analys av den seriella andelen hårdvaruutgift sparad eller slösad, och för en myndighet som svarar för kostnaden för ett offentligt system bör den som godkände den parallella designen kunna visa mätningen som motiverade den vid revision.

Sektorsperspektiv

Startup. Med ett pyttelitet team och ingen löptid att undvara, köp korrekthet med struktur, inte med en samtidighetsspecialist du inte kan rekrytera. Sträck dig efter det enda säkra standardval ditt språk ger, async/await för I/O-bundet arbete, en ägande uppgift eller en aktör för allt delat tillstånd, och hoppa över handjusterad låsning helt. Ett förlorat-uppdatering-kapplöpningstillstånd i en betalningsväg kan sänka dig snabbare än en missad funktion, så spendera den lilla extra koden för att göra den klassen av bugg omöjlig och gå vidare.

Småföretag. Du har ingen vars jobb är samtidighet, så föredra plattformar och hanterade tjänster som hanterar det åt dig: en databastransaktion, en hostad kö eller ett ramverks begäransmodell slår trådar du underhåller för hand. När du utvärderar ett verktyg, behandla “gör detta samtidighet säker som standard” som en fråga om köp mot bygg, och föredra alternativet där en fel sammanflätning inte tyst kan korrumpera en kunds post. Håll delat föränderligt tillstånd borta från din egen kod varhelst en begränsad, hanterad tjänst kan hålla det i stället.

Storföretag. Över många team är målet ett husstandardval som håller tusentals bidragsgivare säkra: oföränderlighet och meddelandeförmedling som norm, modeller på högre nivå framför råa lås, begränsade köer och pooler med mottryck och en dokumenterad global låsordning. Koda dessa i tekniska standarder, upprätthåll dem med kapplöpningsdetektorer och stresstester i CI och styr undantagen där en profilerare motiverade låsfri kod så att var och en förblir bakom en testad, granskad gräns. Hantera samtidighetskapacitet som en flottomfattande angelägenhet med köbegränsningar och poolstorlekar knutna till uppmätt belastning.

Offentlig sektor. Korrekthet och granskningsbarhet i system som körs i decennier väger tyngre än rå genomströmning. Kräv att varje tillståndsövergång registreras och kan spelas upp, så att ett misstänkt kapplöpningstillstånd kan reproduceras och rättelsen bevisas inför ett tillsynsorgan, och behåll AI-fria deterministiska vägar för beslut som rör bidrag, säkerhet eller offentliga handlingar. Upphandling bör kräva att leverantörer redovisar sin samtidighetsmodell och belägg för täckning av kapplöpningsdetektorer och stresstester, eftersom ett felaktigt svar under belastning i ett offentligt system inte är en olägenhet, det är något en ansvarig tjänsteman senare måste förklara.

Exempel

Startup. Ett litet team levererar en betalningsfunktion och märker att kontosaldon ibland driver några ören under belastning. Orsaken är en vanlig läs-ändra-skriv på ett saldofält från samtidiga begäranshanterare, ett förlorad-uppdatering-kapplöpningstillstånd. I stället för att strö lås flyttar de varje kontos saldo bakom en enda ägande uppgift som bearbetar debiteringar och krediteringar som meddelanden, en i taget. Driften försvinner, koden blir lätt att resonera om och de lägger till ett stresstest som avfyrar tusentals samtidiga överföringar för att skydda rättelsen. En strukturell ändring, en hel klass av bugg avvecklad.

Storföretag. En orderservice med hög genomströmning som hanterar tiotusentals begäranden per sekund drabbas av periodiska latensspikar och enstaka krascher av slut på minne under trafiktoppar. Undersökningen hittar en obegränsad arbetskö bakom en trådpool som växer utan gräns när efterfrågan överstiger kapaciteten. Teamet begränsar kön, sätter tak på poolen vid en storlek knuten till kärnantalet och lägger till mottryck som snabbt avvisar överskottsbelastning med ett tydligt fel. Genomströmningen blir förutsägbar, krascherna upphör och ett hett lås på en delad cache ersätts med en låsfri struktur först efter att en profilerare bevisar att konkurrensen är verklig. Säkra standardval för de många författarna, justerad samtidighet bara där det är uppmätt.

Offentlig sektor. En nationell bidragsplattform körs i decennier och måste producera granskningsbara, korrekta resultat även under samtidiga ärendeuppdateringar. Teamet väljer oföränderlighet och meddelandeförmedling som husstandardval, begränsar varje del av föränderligt tillstånd till en enda ägare och inför en global låsordning varhelst lås återstår, allt nedskrivet i de tekniska standarderna. De kör trådsanerare och slumpmässiga stresstester i pipelinen och designar så att varje tillståndsövergång registreras och kan spelas upp för tillsyn, vilket låter dem reproducera och bevisa rättelsen när en sällsynt sammanflätning misstänks. Korrekthet och granskningsbarhet behandlas som förstklassiga krav, inte prestandaeftertankar.

Affärsnytta: motiv, ROI och TCO

Avkastningen på disciplinerad samtidighet syns som incidenter som aldrig inträffar. Ett enda produktionskapplöpningstillstånd kan korrumpera data över tusentals poster, och kostnaden inkluderar både de ingenjörstimmar det tar att hitta en bugg som gömmer sig när den observeras och den långt större kostnaden för att stämma av dåliga data, meddela drabbade användare och bygga upp förtroende på nytt. Det här är bland de dyraste defekterna att diagnostisera just därför att de är icke-deterministiska, så att jaga en heisenbugg kan överstiga insatsen att välja en säker modell i förväg.

Fördelen syns också som genomströmning och kostnad. Rätt dimensionerad samtidighet låter en tjänst hantera långt mer belastning på samma hårdvara, en återkommande besparing för en stor flotta, medan mottryck och begränsade köer förhindrar de kaskaderande avbrott som förvandlar en trafiktopp till en offentlig incident. Den totala ägandekostnaden är måttlig och mest kulturell: ni investerar i en husstil (oföränderlighet, meddelandeförmedling, strukturerad samtidighet), i verktyg (kapplöpningsdetektorer, trådsanerare, stressramverk i CI) och i standarder som kodar låsordning och begränsning. Alternativet är en kodbas där korrekthet beror på att varje författare är expert för alltid, vilket inget växande team kan upprätthålla. Argumentera inför ledningen i deras enheter: översätt ett förhindrat kapplöpningstillstånd till dataförstörande incidenter som undvikits, mottryck till avbrott som förhindrats och ett säkert standardval till sparad introduktionstid.

Antimönster och fallgropar

  • Att lägga till trådar för hastighet på I/O-bundet arbete. Fler trådar på en väntetung tjänst köper konkurrens, inte genomströmning.
  • Delat föränderligt tillstånd överallt. Varje tråd som muterar vilket objekt som helst gör korrekthet till en fråga om tur ingen granskare kan verifiera.
  • Obegränsade köer och pooler. En topp får kön att växa tills minnet dör. Kraschen ser mystisk ut men är ren överbelastning.
  • Ad hoc-låsning utan global ordning. Lås tagna i olika ordning över kodbasen ger baklås under belastning.
  • Att anta att kod körs i skriven ordning. Att ignorera minnesmodellen, så att en synlighetsbugg lämnar en tråd som snurrar på ett inaktuellt värde.
  • Handrullad låsfri klurighet. Egna låsfria scheman är nästan alltid subtilt fel och nästan omöjliga att granska.
  • Att bara testa den lyckade sammanflätningen. Deterministiska tester passerar medan den enda ordningen på en miljon korrumperar produktion.
  • Att anropa okänd kod medan ett lås hålls. Ett återanrop som blockerar eller återinträder förvandlar ett kritiskt avsnitt till ett baklås.

Mognadsmodell

  • Nivå 1, Initiera: Samtidighet är ad hoc och reaktiv. Trådar och lås läggs till på instinkt, delat föränderligt tillstånd finns överallt och köer är obegränsade. Kapplöpningstillstånd dyker upp som oreproducerbara produktionsincidenter ingen kan diagnostisera, och inga verktyg finns för att fånga dem.
  • Nivå 2, Utveckla: Vissa team har lärt sig grundläggande praxis: de använder lås med mer omsorg och begränsar sina mest uppenbara köer. Det finns informell medvetenhet om kapplöpningar och baklås, och några kritiska vägar får extra granskning. Praxis är inkonsekvent över team, testning är fortfarande mest med en sammanflätning och säkra mönster lever hos individer snarare än i skrift.
  • Nivå 3, Standardisera: Organisationen har en dokumenterad husstil som upprätthålls i hela organisationen: oföränderlighet och meddelandeförmedling som standardval, modeller på högre nivå framför råa lås, begränsade köer och pooler med mottryck och en dokumenterad global låsordning. Kapplöpningsdetektorer och stresstester körs i CI, och samtidighetsval följer av om arbetet är I/O-bundet eller CPU-bundet.
  • Nivå 4, Hantera: Organisationen mäter och styr sin samtidighetsställning mot utgångslägen. Den följer täckning av kapplöpningsdetektorer och trådsanerare över tjänster, registrerar könivå, låsvänttid, poolmättnad och avvisningsfrekvenser som bevakade mått och belastningstestar degraderingskurvan så att varje gräns är ett databaserat kapacitetsbeslut. Samtidighetsincidenter räknas och trendas, seriella andelar av parallelliserade arbetsbelastningar mäts mot den uppsnabbning som faktiskt uppnåtts och beslut att gå eller inte gå vidare med nya designer vilar på det beläggen snarare än instinkt.
  • Nivå 5, Orkestrera: Säker samtidighet är den väg som kräver minst motstånd för varje författare, och praxis förbättras kontinuerligt och är integrerad i hela organisationen. Hela buggklasser är omöjliga per konstruktion, heta punkter justeras bara där profilering bevisar det och deterministisk uppspelning gör den sällsynta kvarvarande buggen reproducerbar. Korrekthet och granskningsbarhet är kontinuerligt försvarade egenskaper, kapacitetsgränser anpassas efter observerad belastning och standarderna utvecklas när plattformen och arbetsbelastningen förskjuts.

Idéer för diskussion

  1. Om ni reviderade er mest trafikerade tjänst i dag, hur mycket av dess tillstånd är delat och föränderligt, och hur mycket av den delningen är verkligen nödvändig?
  2. Vilket är ert teams standardsvar när någon behöver att två uppgifter samordnas, och skulle ni hellre att det var oföränderlighet eller meddelandeförmedling?
  3. Var gömmer sig obegränsade köer eller pooler utan tak fortfarande i ert system, och vad skulle hända med dem under en plötslig tiofaldig trafiktopp?
  4. Inkluderar era körningar av kontinuerlig integration en kapplöpningsdetektor eller trådsanerare, och när fångade en senast något före produktion?
  5. För er mest parallelliserade arbetsbelastning, vad är den seriella andelen, och sätter Amdahls lag tak för den uppsnabbning ni faktiskt jagar?
  6. Kunde ert team reproducera en sammanflätningsbugg på en miljon på begäran, och vad skulle krävas för att komma dit?

Viktigaste punkter

  • Samtidighet strukturerar ett program som oberoende uppgifter. Parallellism kör dem på en gång. Besluta vilken du behöver innan du lägger till trådar.
  • Delat föränderligt tillstånd är roten till nästan varje samtidighetsbugg. Föredra oföränderlighet och meddelandeförmedling som säkra standardval för många författare.
  • Sträck dig efter modeller på högre nivå (aktörer, kanaler, strukturerad samtidighet, async/await) före handskrivna lås, som är korrekta i teorin och farliga i praktiken.
  • Förstå atomicitet, synlighet och din minnesmodell. Använd rätt primitiv, håll lås kort och inför en global låsordning för att undvika baklås, livelock och svält.
  • Begränsa varje kö och pool och tillämpa mottryck, så att en topp degraderar graciöst i stället för att krascha (kapitel 3.3).
  • Testa sammanflätningarna med avsikt med kapplöpningsdetektorer, stresstester och uppspelning (kapitel 2.15), och respektera Amdahls lag när du parallelliserar (kapitel 2.16).
  • För företag är detta genomströmning och förhindrade incidenter. För myndigheter är det korrekthet och granskningsbarhet i långlivade system.

Referenser och vidare 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.