Låt oss gå Golang Concurrency, Del 1

Översikt

Varje framgångsrikt programmeringsspråk har en del mördarfunktion som gjorde det lyckat. Go's forte är samtidig programmering. Det var utformat kring en stark teoretisk modell (CSP) och tillhandahåller språknivåsyntax i form av "go" -ordet som startar en asynkron uppgift (ja språket är namngivet efter sökordet) samt inbyggt sätt att kommunicera mellan samtidiga uppgifter. 

I den här artikeln (del ett) introducerar jag CSP-modellen som Gos samtidiga redskap, goroutines och hur man synkroniserar operationen av flera samarbetsvilliga goroutiner. I en framtida artikel (del två) ska jag skriva om Gos kanaler och hur man koordinerar mellan goroutines utan synkroniserade datastrukturer.

CSP

CSP står för kommunicera sekventiella processer. Det introducerades första gången av Tony (C. A. R.) Hoare 1978. CSP är en högnivåram för att beskriva samtidiga system. Det är mycket lättare att programmera korrekta samtidiga program vid drift på CSP-abstraktionsnivån än vid de typiska trådarna och låsnings abstraktionsnivån.

Goroutines

Goroutines är ett spel på coroutines. Men de är inte exakt samma. En goroutin är en funktion som utförs på en separat tråd från startgängan, så den blockerar inte den. Flera goroutiner kan dela samma OS-tråd. Till skillnad från koroutiner kan goroutiner inte uttryckligen ge kontroll över en annan goroutin. Go's runtime tar hand om implicit överföring av kontroll när en viss goroutine skulle blockera på I / O-åtkomst. 

Låt oss se någon kod. Go-programmet nedan definierar en funktion, kreativt kallad "f", som sover slumpmässigt upp till en halv sekund och skriver sedan ut sitt argument. De main () funktionen kallar f () funktion i en loop med fyra iterationer, där i varje iteration det ringer f () tre gånger med "1", "2" och "3" i rad. Som du förväntar dig är utmatningen:

--- Kör sekventiellt som normala funktioner 1 2 3 1 2 3 1 2 3 1 2 3

Då åberopar huvud f () som en goroutin i en liknande slinga. Nu är resultaten olika eftersom Go's runtime körs f goroutines samtidigt, och sedan den slumpmässiga sömn är annorlunda mellan goroutinerna, sker inte trycket av värdena i ordningen f () åberopades. Här är utgången:

--- Kör samtidigt som goroutiner 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3

Programmet använder sig av "tid" och "matte / rand" standardbibliotekspaket för att genomföra slumpmässigt sova och vänta i slutet för alla goroutiner att slutföra. Detta är viktigt för att när huvudgängan går ut, är programmet gjort, även om det finns enastående goroutiner som fortfarande körs.

paketet main import ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (s string) // Sova upp till en halv sekund fördröjning: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (fördröjning) fmt.Println (s) func main () fmt.Println ("--- Kör sekventiellt som vanliga funktioner ") för i: = 0; jag < 4; i++  f("1") f("2") f("3")  fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++  go f("1") go f("2") go f("3")  // Wait for 6 more seconds to let all go routine finish time.Sleep(time.Duration(6) * time.Second) fmt.Println("--- Done.") 

Synkroniseringsgrupp

När du har en massa vilda goroutiner som kör överallt, vill du ofta veta när de är färdiga. 

Det finns olika sätt att göra det, men ett av de bästa sätten är att använda a WaitGroup. en WaitGroup är en typ som definieras i "synkroniseringspaketet" som tillhandahåller Lägg till(), Gjort() och Vänta() operationer. Det fungerar som en räknare som räknar hur många går rutiner som fortfarande är aktiva och väntar tills de är alla färdiga. När du startar en ny goroutine, ringer du Tillsätt (1) (du kan lägga till fler än en om du startar flera go rutiner). När en goroutin är klar kallas den Gjort(), vilket minskar räkningen med en, och Vänta() blockera tills räkningen når noll. 

Låt oss konvertera det föregående programmet för att använda a WaitGroup istället för att sova i sex sekunder bara i slutändan. Observera att f () funktionsanvändning skjuta upp wg.Done () istället för att ringa wg.Done () direkt. Detta är användbart för att säkerställa wg.Done () kallas alltid, även om det finns ett problem och goroutinen slutar tidigt. I annat fall kommer räkningen aldrig att nå noll, och wg.Wait () kan blockera för alltid.

Ett annat litet knep är det jag kallar wg.Add (3) bara en gång innan man åberopar f () tre gånger. Observera att jag ringer wg.Add () även när man åberopar f () som en vanlig funktion. Detta är nödvändigt eftersom f () samtal wg.Done () oavsett om det går som en funktion eller goroutin.

paketets huvudsakliga import ("fmt" "time" "math / rand" "synkroniserings") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f sträng) defer wg.Done () // Sömn upp till en halv sekund fördröjning: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Kör sekventiellt som normala funktioner") för i: = 0; jag < 4; i++  wg.Add(3) f("1") f("2") f("3")  fmt.Println("--- Run concurrently as goroutines") for i := 0; i < 5; i++  wg.Add(3) go f("1") go f("2") go f("3")  wg.Wait() 

Synkroniserade datastrukturer

Goroutinerna i 1,2,3-programmet kommunicerar inte med varandra eller fungerar på delade datastrukturer. I den verkliga världen är det ofta nödvändigt. "Sync" -paketet levererar Mutex-typen Låsa() och Låsa upp() metoder som ger ömsesidig uteslutning. Ett bra exempel är standard Go-kartan. 

Det är inte synkroniserat med design. Det betyder att om flera goroutiner kommer åt samma karta samtidigt utan extern synkronisering, blir resultaten oförutsägbara. Men om alla goroutiner är överens om att förvärva en gemensam mutex före varje åtkomst och släppa den senare kommer åtkomst att bli serialiserad.

Få alltid att falla på plats

Låt oss lägga allt ihop. Den berömda Tour of Go har en övning för att bygga en webbrobot. De ger en bra ram med en mock Fetcher och resultat som låter dig fokusera på problemet vid handen. Jag rekommenderar starkt att du försöker lösa det själv.

Jag skrev en komplett lösning med två metoder: en synkroniserad karta och kanaler. Den fullständiga källkoden finns här.

Här är relevanta delar av synkroniseringslösningen. Låt oss först definiera en karta med en mutex-struktur för att hålla hämtade URL-adresser. Notera den intressanta syntaxen där en anonym typ skapas, initialiseras och tilldelas en variabel i ett uttalande.

var fetchedUrls = struct urls map [string] bool m sync.Mutex urls: make (map [string] bool)

Nu kan koden låsa m mutex innan du öppnar kartan över webbadresser och låser upp när den är klar.

// Kontrollera om den här webbadressen redan har hämtats (eller hämtas) fetchedUrls.m.Lock () om fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK. Låt oss hämta denna url fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()

Det här är inte helt säkert eftersom någon annan kan komma åt fetchedUrls variabel och glöm inte att låsa eller låsa upp. En mer robust design kommer att ge en datastruktur som stöder säkra operationer genom att låsa / låsa upp automatiskt.

Slutsats

Go har utmärkt stöd för samtidighet med lätta goroutiner. Det är mycket lättare att använda än traditionella trådar. När du behöver synkronisera åtkomst till delade datastrukturer, har Go din rygg med sync.Mutex

Det finns mycket mer att berätta om Gos samtidighet. Håll dig igång ...