Let's Go Golang Concurrency, deel 1

Overzicht

Elke succesvolle programmeertaal heeft een geweldige functie die het succesvol heeft gemaakt. Go's forte is gelijktijdige programmering. Het is ontworpen om een ​​sterk theoretisch model (CSP) en biedt syntaxis op taalniveau in de vorm van het "go" -woord dat een asynchrone taak start (ja, de taal is naar het trefwoord genoemd) en een ingebouwde manier om te communiceren tussen gelijktijdige taken. 

In dit artikel (deel één) zal ik het CSP-model introduceren dat Go's concurrency implementeert, goroutines, en hoe de werking van meerdere samenwerkende goroutines kan worden gesynchroniseerd. In een toekomstig artikel (deel twee) zal ik schrijven over de kanalen van Go en hoe te coördineren tussen goroutines zonder gesynchroniseerde datastructuren.

CSP

CSP staat voor Communicating Sequential Processes. Het werd voor het eerst geïntroduceerd door Tony (C. A.R.) Hoare in 1978. CSP is een raamwerk op hoog niveau voor het beschrijven van gelijktijdige systemen. Het is veel gemakkelijker om correcte gelijktijdige programma's te programmeren wanneer op het niveau van de CSP-abstractie wordt gewerkt dan op het typische abstractieniveau voor threads en sloten.

Goroutines

Goroutines zijn een spel op coroutines. Ze zijn echter niet precies hetzelfde. Een goroutine is een functie die wordt uitgevoerd op een afzonderlijke thread van de startdraad, dus deze blokkeert deze niet. Meerdere goroutines kunnen dezelfde OS-thread delen. In tegenstelling tot coroutines, kunnen goroutines niet expliciet controle overgeven aan een andere goroutine. Go's runtime zorgt ervoor dat de controle impliciet wordt overgedragen wanneer een bepaalde goroutine de I / O-toegang blokkeert. 

Laten we wat code zien. Het Go-programma hieronder definieert een functie, met de creatieve naam "f", die willekeurig tot een halve seconde slaapt en vervolgens het argument ervan afdrukt. De hoofd() functie roept de f () functie in een lus van vier iteraties, waarbij in elke iteratie wordt geroepen f () drie keer met "1", "2" en "3" op een rij. Zoals je zou verwachten, is de output:

--- Voer opeenvolgend uit als normale functies 1 2 3 1 2 3 1 2 3 1 2 3

Vervolgens roept main op f () als een goroutine in een vergelijkbare lus. Nu zijn de resultaten anders omdat Go's runtime de f goroutines gelijktijdig, en dan omdat de willekeurige slaap verschillend is tussen de goroutines, gebeurt het afdrukken van de waarden niet in de volgorde f () werd aangeroepen. Dit is de uitvoer:

--- Voer gelijktijdig uit als goroutines 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3

Het programma zelf gebruikt de standaardbibliotheekpakketten "tijd" en "wiskunde / rand" om het willekeurige slapen te implementeren en uiteindelijk te wachten tot alle goroutines zijn voltooid. Dit is belangrijk omdat, wanneer de hoofdthread wordt afgesloten, het programma is voltooid, zelfs als er nog uitstekende goroutines actief zijn.

pakket main import ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) func f (s string) // slaap tot een halve seconde vertraging: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Run sequentially als normale functies ") voor i: = 0; ik < 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.") 

Groep synchroniseren

Als je een stel wilde goroutines hebt die helemaal over de vloer lopen, wil je vaak weten wanneer ze allemaal klaar zijn. 

Er zijn verschillende manieren om dat te doen, maar een van de beste benaderingen is om een WaitGroup. EEN WaitGroup is een type dat is gedefinieerd in het "synchronisatie" -pakket dat de Toevoegen(), Gedaan() en Wacht() activiteiten. Het werkt als een teller die telt hoeveel routines nog steeds actief zijn en wacht totdat ze allemaal klaar zijn. Wanneer je een nieuwe goroutine start, bel je Voeg (1) (u kunt meer dan één toevoegen als u meerdere routines start). Als een goroutine klaar is, roept hij Gedaan(), wat de telling met één vermindert, en Wacht() blokken totdat de telling nul bereikt. 

Laten we het vorige programma converteren om a te gebruiken WaitGroup in plaats van zes seconden te slapen, voor het geval het er uiteindelijk uitziet. Merk op dat de f () functie gebruikt uitstellen wg.Done () in plaats van te bellen wg.Done () direct. Dit is handig om te zorgen wg.Done () wordt altijd genoemd, zelfs als er een probleem is en de goroutine vroegtijdig wordt beëindigd. Anders zal de telling nooit nul bereiken, en wg.Wait () kan voor altijd blokkeren.

Een andere kleine truc is dat ik bel wg.Add (3) één keer voor het aanroepen f () drie keer. Merk op dat ik bel wg.Add () zelfs bij het aanroepen f () als een reguliere functie. Dit is noodzakelijk omdat f () calls wg.Done () ongeacht of het als een functie of goroutine werkt.

pakket main import ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now (). UnixNano ())) var wg sync.WaitGroup func f (s string) uitstellen wg.Done () // Slaap maximaal een halve seconde vertraging: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (delay) fmt.Println (s) func main () fmt.Println ("--- Run sequentially as normal functions") voor i: = 0; ik < 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() 

Gesynchroniseerde gegevensstructuren

De goroutines in het 1,2,3-programma communiceren niet met elkaar of werken niet op gedeelde datastructuren. In de echte wereld is dit vaak nodig. Het "sync" -pakket biedt het Mutex-type met Slot() en Unlock () methoden die wederzijdse uitsluiting bieden. Een goed voorbeeld is de standaard Go-kaart. 

Het is niet gesynchroniseerd door ontwerp. Dat betekent dat als meerdere goroutines gelijktijdig toegang hebben tot dezelfde kaart zonder externe synchronisatie, de resultaten onvoorspelbaar zullen zijn. Maar als alle goroutines overeenkomen om voor elke toegang een gedeelde mutex te verkrijgen en deze later vrij te geven, dan zal de toegang worden geserialiseerd.

Alles samenvoegen

Laten we het allemaal samenvoegen. De beroemde Tour of Go heeft een oefening om een ​​webcrawler te maken. Ze bieden een geweldig raamwerk met een nep-fetcher en resultaten waarmee je je op het probleem kunt concentreren. Ik beveel ten zeerste aan dat je het zelf probeert op te lossen.

Ik schreef een complete oplossing met twee benaderingen: een gesynchroniseerde kaart en kanalen. De volledige broncode is hier beschikbaar.

Hier zijn de relevante delen van de "sync" -oplossing. Laten we eerst een kaart definiëren met een mutex struct om opgehaalde URL's te bewaren. Let op de interessante syntaxis waarin een anoniem type wordt gemaakt, geïnitialiseerd en toegewezen aan een variabele in één instructie.

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

Nu kan de code de vergrendeling vergrendelen m mutex voordat u de kaart met URL's opent en ontgrendelt als het klaar is.

// Controleer of deze URL al is opgehaald (of wordt opgehaald) fetchedUrls.m.Lock () if fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK. Laten we deze URL ophalen fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()

Dit is niet helemaal veilig omdat iemand anders toegang heeft tot de fetchedUrls variabel en vergeet te vergrendelen of ontgrendelen. Een robuuster ontwerp biedt een datastructuur die veilige bewerkingen ondersteunt door het automatisch vergrendelen / ontgrendelen.

Conclusie

Go heeft uitstekende ondersteuning voor concurrency met behulp van lichtgewicht goroutines. Het is veel gemakkelijker te gebruiken dan traditionele threads. Wanneer u de toegang tot gedeelde datastructuren moet synchroniseren, heeft Go uw rug bij de sync.Mutex

Er is nog veel meer te vertellen over Go's concurrency. Blijf kijken…