La oss gå Golang Concurrency, Del 1

Oversikt

Hvert vellykket programmeringsspråk har noen morderfunksjon som gjorde det vellykket. Go's forte er samtidig programmering. Den ble designet for å omgå en sterk teoretisk modell (CSP) og gir språknivåsyntax i form av "go" -ordet som starter en asynkron oppgave (ja språket er oppkalt etter søkeordet), samt en innebygd måte å kommunisere mellom samtidige oppgaver. 

I denne artikkelen (del ett), introduserer jeg CSP-modellen som Gos samtidige redskaper, goroutines, og hvordan du synkroniserer driften av flere samarbeidende goroutiner. I en fremtidig artikkel (del to) skal jeg skrive om Go's kanaler og hvordan man kan koordinere mellom goroutiner uten synkroniserte datastrukturer.

CSP

CSP står for kommunikasjon av sekvensielle prosesser. Det ble først introdusert av Tony (C. A. R.) Hoare i 1978. CSP er et høyt nivå rammeverk for å beskrive samtidige systemer. Det er mye lettere å programmere riktige samtidige programmer når de opererer på CSP-abstraksjonsnivået enn ved typiske tråder og låser abstraksjonsnivå.

Goroutines

Goroutines er et spill på coroutines. Men de er ikke akkurat det samme. En goroutine er en funksjon som utføres på en separat tråd fra lanseringen, slik at den ikke blokkerer den. Flere goroutiner kan dele samme OS-tråd. I motsetning til koroutiner, kan goroutiner ikke eksplisitt gi kontroll til en annen goroutin. Go's runtime tar seg av implisitt å overføre kontroll når en bestemt goroutine vil blokkere på I / O-tilgang. 

La oss se noen kode. Go-programmet nedenfor definerer en funksjon, kreativt kalt "f", som sover tilfeldig opptil et halvt sekund og deretter skriver ut argumentet. De hoved() funksjonen ringer på f () Fungerer i en løkke med fire iterasjoner, hvor i hver iterasjon det ringer f () tre ganger med "1", "2" og "3" på rad. Som du forventer, er produksjonen:

--- Kjør sekvensielt som normale funksjoner 1 2 3 1 2 3 1 2 3 1 2 3

Da hevder hovedansvarlig f () som en goroutin i en lignende sløyfe. Nå er resultatene forskjellige fordi Gos kjøretid kjører f goroutiner samtidig, og da den tilfeldige søvn er forskjellig mellom goroutinene, skjer ikke trykket av verdiene i rekkefølgen f () ble påkalt. Her er utgangen:

--- Kjør samtidig som goroutiner 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3

Programmet i seg selv bruker "tid" og "matte / rand" standardbibliotekspakker for å implementere tilfeldig søvn og venter til slutt for alle goroutiner å fullføre. Dette er viktig fordi når hovedtråden går ut, er programmet ferdig, selv om det fortsatt finnes utestående goroutiner.

pakke main import ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (s streng) // Sov opp til en halv sekund forsinkelse: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (forsinkelse) fmt.Println (s) func main () fmt.Println ("--- Kjør sekvensielt som normale funksjoner ") for i: = 0; Jeg < 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.") 

Synkroniseringsgruppe

Når du har en haug med vilde goroutiner som kjører over alt, vil du ofte vite når de er ferdige. 

Det er forskjellige måter å gjøre det på, men en av de beste tilnærmingene er å bruke en WaitGroup. EN WaitGroup er en type som er definert i "synkroniseringspakken" som gir Legg til(), Ferdig () og Vente() operasjoner. Det fungerer som en teller som teller hvor mange går rutiner er fortsatt aktive og venter til de er ferdige. Når du starter en ny goroutine, ringer du Legg til (1) (du kan legge til flere enn én hvis du starter flere rutiner). Når en goroutin er ferdig, kaller den Ferdig (), som reduserer tellingen med en, og Vente() blokkerer til tellingen når null. 

La oss konvertere det forrige programmet for å bruke en WaitGroup i stedet for å sove i seks sekunder, bare i tilfelle til slutt. Legg merke til at f () funksjon bruker utsette wg.Done () i stedet for å ringe wg.Done () direkte. Dette er nyttig for å sikre wg.Done () er alltid kalt, selv om det er et problem og goroutinen slutter tidlig. Ellers vil tellingen aldri nå null, og wg.Wait () kan blokkere for alltid.

Et annet lite knep er det jeg kaller wg.Add (3) bare en gang før påkalling f () tre ganger. Legg merke til at jeg ringer wg.Add () selv når man påberoper seg f () som en vanlig funksjon. Dette er nødvendig fordi f () samtaler wg.Done () uansett om det går som en funksjon eller goroutin.

pakke main import ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f streng) defer wg.Done () // Sov opptil en halv sekund forsinkelse: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (forsinkelse) fmt.Println (s) func main () fmt.Println ("--- Kjør sekvensielt som normale funksjoner") for i: = 0; Jeg < 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() 

Synkroniserte datastrukturer

Goroutinene i 1,2,3-programmet kommuniserer ikke med hverandre eller opererer på delte datastrukturer. I den virkelige verden er dette ofte nødvendig. "Synkroniseringspakken" gir Mutex-typen med Låse() og Låse opp() metoder som gir gjensidig utestenging. Et godt eksempel er standard Go-kartet. 

Det er ikke synkronisert med design. Det betyr at hvis flere goroutiner får tilgang til det samme kartet samtidig uten ekstern synkronisering, blir resultatene uforutsigbare. Men hvis alle goroutinene er enige om å skaffe seg en felles mutex før hver tilgang og slipp den senere, vil tilgangen bli serialisert.

Sette alt sammen

La oss sette alt sammen. Den berømte Tour of Go har en øvelse for å bygge en web crawler. De gir et flott rammeverk med en mock Fetcher og resultater som lar deg fokusere på problemet ved hånden. Jeg anbefaler at du prøver å løse det selv.

Jeg skrev en komplett løsning ved hjelp av to tilnærminger: et synkronisert kart og kanaler. Den komplette kildekoden er tilgjengelig her.

Her er de relevante delene av "synkroniseringsløsningen". Først, la oss definere et kart med en mutex struct for å holde hentet nettadresser. Merk det interessante syntaksen der en anonym type opprettes, initialiseres og tilordnes en variabel i en setning.

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

Nå kan koden låse m mutex før du får tilgang til kartet over nettadresser og låser opp når det er gjort.

// Sjekk om denne nettadressen allerede er hentet (eller hentes) hentetUrls.m.Lock () hvis hentetUrls.urls [url] fetchedUrls.m.Unlock () return // OK. La oss hente denne url hentetUrls.urls [url] = true fetchedUrls.m.Unlock ()

Dette er ikke helt trygt fordi noen andre har tilgang til fetchedUrls variabel og glem å låse eller låse opp. En mer robust design vil gi en datastruktur som støtter sikker drift ved å låse / låse opp automatisk.

Konklusjon

Go har ypperlig støtte for samtidighet ved bruk av lette goroutiner. Det er mye lettere å bruke enn tradisjonelle tråder. Når du trenger å synkronisere tilgang til delte datastrukturer, har Go din rygg med sync.Mutex

Det er mye mer å fortelle om Go's samtidighet. Følg med…