Fiecare limbaj de programare de succes are o caracteristică ucigașă care a reușit. Go's forte este o programare concurentă. Acesta a fost conceput astfel încât să cuprindă un model teoretic puternic (CSP) și oferă sintaxă la nivel de limbă sub forma cuvântului cheie "go" care inițiază o sarcină asincronă (da, limba este denumită după cuvântul cheie), precum și un mod încorporat pentru a comunica între sarcinile concurente.
În acest articol (partea întâi), voi introduce modelul CSP pe care îl implementează concurența Go, gorutinele și cum se poate sincroniza funcționarea mai multor gorutine care cooperează. Într-un articol viitor (partea a doua), voi scrie despre canalele lui Go și cum să coordonez între gorutinele fără structuri de date sincronizate.
CSP reprezintă comunicarea proceselor secvențiale. A fost inițial introdus de Tony (C. A. R.) Hoare în 1978. CSP este un cadru la nivel înalt pentru descrierea sistemelor concurente. Este mult mai ușor să programați programe corecte corecte atunci când operează la nivelul de abstractizare CSP decât la nivelul tipic al firelor și blochează nivelul de abstractizare.
Gorutinele sunt o piesă despre corutine. Cu toate acestea, ele nu sunt exact la fel. O gorutină este o funcție care este executată pe un fir separat de firul de lansare, astfel încât să nu o blocheze. Grute-urile multiple pot partaja același thread de sistem de operare. Spre deosebire de corutine, gorutinele nu pot să dea control în mod explicit unei alte gorutine. Go's runtime are grijă de transferul implicit al controlului atunci când o anumită gorutină ar bloca accesul la I / O.
Să vedem un cod. Programul Go de mai jos definește o funcție, denumită în mod creativ "f", care doarme întâmplător până la o jumătate de secundă și apoi imprimă argumentul său. principal()
funcția solicită f ()
funcționează într-o buclă de patru iterații, unde, în fiecare iterație, operează f ()
de trei ori cu "1", "2" și "3" la rând. După cum v-ați aștepta, rezultatul este:
--- Rulați secvențial funcțiile normale 1 2 3 1 2 3 1 2 3 1 2 3
Atunci invocă principalul f ()
ca o gorutină într-o buclă similară. Rezultatele sunt diferite, deoarece runtime-ul Go va executa f
gorutinele concomitent, și apoi, din moment ce somnul aleator este diferit între gorutine, imprimarea valorilor nu se întâmplă în ordinea f ()
a fost invocată. Aici este rezultatul:
--- Fugiți simultan ca gorutine 2 2 3 1 3 2 1 3 1 1 3 2 2 1 3
Programul însuși folosește pachetele standard de timp "și" math / rand "pentru a implementa somnul aleatoriu și așteptarea în cele din urmă pentru ca toate gorutinele să se finalizeze. Acest lucru este important deoarece, atunci când firul principal iese, programul se termină, chiar dacă există încă gorutine restante.
pachetul principal de import ("fmt" "time" "math / rand") var r = rand.New (rand.NewSource (time.Now (). o jumatate de secunda intarziere: = time.Duration (r.Int ()% 500) * time.Millisecond time.Sleep (intarziere) fmt.Println (s) func main () fmt.Println ca funcții normale ") pentru i: = 0; eu < 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.")
Când ai o grămadă de gorutine sălbatice care rulează peste tot, adesea vrei să știi când sunt toate terminate.
Există modalități diferite de a face acest lucru, dar una dintre cele mai bune abordări este utilizarea unui a WaitGroup
. A WaitGroup
este un tip definit în pachetul "sincronizare" care oferă Adăuga()
, Terminat()
și Aștepta()
operațiuni. Funcționează ca un contor care contează cât de multe rutine sunt încă active și așteaptă până când sunt toate terminate. Ori de câte ori porniți o nouă gorutină, sunați Adăugați (1)
(puteți adăuga mai mult de unul dacă lansați rutine multiple). Când se face o gorutină, se cheamă Terminat()
, ceea ce reduce numărarea cu unul, și Aștepta()
blochează până când numărul ajunge la zero.
Să convertim programul anterior pentru a folosi a WaitGroup
în loc de a dormi timp de șase secunde doar în cazul în care în cele din urmă. Rețineți că f ()
funcții defer wg.Done ()
în loc de a chema wg.Done ()
direct. Acest lucru este util pentru a vă asigura wg.Done ()
este numit întotdeauna, chiar dacă există o problemă și gorutina se termină mai devreme. În caz contrar, contele nu va ajunge niciodată la zero și wg.Wait ()
poate bloca pentru totdeauna.
Un alt truc mic îl numesc wg.Add (3)
doar o dată înainte de invocare f ()
de trei ori. Rețineți că sună wg.Add ()
chiar și atunci când invocă f ()
ca o funcție obișnuită. Acest lucru este necesar deoarece f ()
apeluri wg.Done ()
indiferent dacă funcționează ca o funcție sau gorutină.
pachetul principal de import ("fmt" "time" "math / rand" "sync") var r = rand.New (rand.NewSource (time.Now () UnixNano ())) var wg sync.WaitGroup func f șir) defer wg.Done () // Sleep până la o jumătate de secundă întârziere: = time.Durație (r.Int ()% 500) * time.Millisecond time.Sleep (întârziere) fmt.Println (s) func main () fmt.Println ("--- Rulați secvențial ca funcții normale") pentru i: = 0; eu < 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()
Autoritățile din programul 1,2,3 nu comunică între ele sau nu funcționează pe structuri de date partajate. În lumea reală, acest lucru este adesea necesar. Pachetul "sincronizare" furnizează tipul Mutex cu Blocare()
și Deblocare ()
metode care oferă excludere reciprocă. Un exemplu excelent este harta standard Go.
Nu este sincronizat prin design. Asta inseamna ca daca mai multe gorutine acceseaza aceeasi harta simultan fara sincronizare externa, rezultatele vor fi imprevizibile. Dar, dacă toate gorutinele sunt de acord să achiziționeze un mutex partajat înainte de fiecare accesare și să îl elibereze mai târziu, atunci accesul va fi serializat.
Să punem totul împreună. Celebrul Tour of Go are un exercițiu de construire a unui crawler web. Acestea oferă un cadru minunat cu un Fetcher mock și rezultate care vă permit să vă concentrați asupra problemei la îndemână. Vă recomandăm foarte mult să încercați să o rezolvați singur.
Am scris o soluție completă folosind două abordări: o hartă și canale sincronizate. Codul sursă complet este disponibil aici.
Iată părțile relevante ale soluției "sincronizare". Mai întâi, să definim o hartă cu un struct de mutex care să dețină URL-urile preluate. Rețineți sintaxa interesantă în cazul în care un tip anonim este creat, inițializat și atribuit unei variabile într-o singură declarație.
var fetchedUrls = struct urls map [șir] bool m sync.Mutex urls: make (hartă [șir] bool)
Acum, codul poate bloca codul m
mutex înainte de a accesa harta hărților de adrese URL și de a le debloca când sa terminat.
// Verificați dacă această adresă URL a fost deja preluată (sau preluată) fetchedUrls.m.Lock () dacă fetchedUrls.urls [url] fetchedUrls.m.Unlock () return // OK. Să preluăm această url fetchedUrls.urls [url] = true fetchedUrls.m.Unlock ()
Acest lucru nu este complet sigur, deoarece oricine altcineva poate accesa fetchedUrls
variabile și uitați să le blocați sau să le deblocați. Un design mai robust va oferi o structură de date care să susțină operațiunile sigure făcând automat blocarea / deblocarea.
Go are un suport excelent pentru concurrency folosind gorutine ușoare. Este mult mai ușor de folosit decât firele tradiționale. Când trebuie să sincronizați accesul la structurile de date partajate, Go are back-ul dvs. cu sync.Mutex
.
Mai sunt multe de spus despre concurența lui Go. Rămâneți aproape…