Una dintre caracteristicile unice ale Go este utilizarea canalelor pentru a comunica în siguranță între gorutine. În acest articol, veți afla ce canale sunt, cum să le utilizați în mod eficient și unele modele comune.
Un canal este o coadă de sincronizare în memorie pe care le pot folosi funcțiile obișnuite pentru a trimite și primi valori tipizate. Comunicarea este serializată prin canal.
Creați un canal utilizând face()
și specificați tipul de valori acceptate de canal:
ch: = face (chan int)
Go oferă o sintaxă de săgeată drăguță pentru trimiterea și primirea către / de la canale:
// trimiteți o valoare unui canal ch <- 5 // receive value from a channel x := <- ch
Nu trebuie să consumi valoarea. Este ok doar pentru a afișa o valoare dintr-un canal:
<-ch
Canalele blochează în mod prestabilit. Dacă trimiteți o valoare unui canal, veți bloca până când cineva îl primește. În mod similar, dacă primiți de la un canal, veți bloca până când cineva trimite o valoare canalului.
Următorul program demonstrează acest lucru. principal()
funcția face un canal și pornește o rutină numită care tipărește "start", citește o valoare din canal și imprimă și ea. Atunci principal()
incepe o alta gorutina care imprima o bara ("-") in fiecare secunda. Apoi, ea doarme 2,5 secunde, trimite o valoare canalului și doarme încă 3 secunde pentru a permite tuturor gorutinelor să termine.
(chan int) // Începeți o gorutină care citește o valoare dintr-un canal și o tipărește func (ch chan int) fmt.Println (" start ") fmt.Println (<-ch) (ch) // Start a goroutine that prints a dash every second go func() for i := 0; i < 5; i++ time.Sleep(time.Second) fmt.Println("-") () // Sleep for two seconds time.Sleep(2500 * time.Millisecond) // Send a value to the channel ch <- 5 // Sleep three more seconds to let all goroutines finish time.Sleep(3 * time.Second)
Acest program demonstrează foarte bine caracterul blocant al canalului. Prima gorutină imprimă imediat "start", dar apoi este blocată pe încercarea de a primi de la canal până la principal()
funcția, care doarme 2,5 secunde și trimite valoarea. Altă gorutină oferă doar o indicație vizuală a fluxului de timp imprimând o bordură regulat în fiecare secundă.
Aici este rezultatul:
începe - - 5 - - -
Acest comportament strâns împerechează expeditorii către receptoare și uneori nu este ceea ce doriți. Go oferă mai multe mecanisme pentru a rezolva problema.
Canalele buffered sunt canale care pot deține un anumit număr (predefinit) de valori, astfel încât expeditorii să nu blocheze până când tamponul este plin, chiar dacă nimeni nu primește.
Pentru a crea un canal tampon, trebuie doar să adăugați o capacitate ca al doilea argument:
ch: = face (chan int, 5)
Următorul program ilustrează comportamentul canalelor tamponate. principal()
programul definește un canal tamponat cu o capacitate de 3. Atunci începe o gorutină care citește un buffer din canal în fiecare secundă și tipărește și o altă gorutină care imprimă o bordură în fiecare secundă pentru a da o indicație vizuală a progresului timpului. Apoi, acesta trimite cinci valori pe canal.
(chan int, 3) // Pornește o gorutină care citește o valoare din canal în fiecare secundă și tipărește funcția func (ch chan int) pentru (chm int) time.Sleep (time.Second) fmt.Printf ("Gorutine a primit:% d \ n", <-ch) (ch) // Start a goroutine that prints a dash every second go func() for i := 0; i < 5; i++ time.Sleep(time.Second) fmt.Println("-") () // Push values to the channel as fast as possible for i := 0; i < 5; i++ ch <- i fmt.Printf("main() pushed: %d\n", i) // Sleep five more seconds to let all goroutines finish time.Sleep(5 * time.Second)
Ce se întâmplă în timpul execuției? Primele trei valori sunt tamponate imediat de canal, iar principal()
blocuri de funcții. După o secundă, o valoare este primită de către gorutină și principal()
funcția poate împinge o altă valoare. O altă secundă trece, gorutina primește o altă valoare, iar principal()
funcția poate împinge ultima valoare. În acest moment, gorutina continuă să primească valori din canal în fiecare secundă.
Aici este rezultatul:
(1) main () împins: 0 main () împins: 1 main () împins: - Gorutine a primit: 4
Canalele buffered (atâta timp cât tamponul este suficient de mare) pot aborda problema fluctuațiilor temporare în cazul în care nu există destule receptoare pentru a procesa toate mesajele trimise. Dar există și problema opusă a receptorilor blocați în așteptarea procesării mesajelor. Du-te te-a acoperit.
Dacă vrei ca gorutina ta să facă altceva când nu există mesaje de procesat într-un canal? Un bun exemplu este dacă receptorul dvs. așteaptă mesaje de la mai multe canale. Nu doriți să blocați pe canalul A dacă canalul B are mesaje acum. Următorul program încearcă să calculeze suma de 3 și 5 utilizând puterea completă a mașinii.
Ideea este de a simula o operație complexă (de ex. O interogare la distanță către un DB distribuit) cu redundanță. sumă()
(notați cum este definită funcția imbricată în interior principal()
) acceptă doi parametri int și returnează un canal int. O gorutină internă anonimă doarme un timp aleator până la o secundă și apoi scrie suma la canal, o închide și o returnează.
Acum, apelurile principale suma (3, 5)
de patru ori și stochează canalele rezultate în variabilele ch1 la ch4. Cele patru apeluri către sumă()
întoarce-te imediat pentru că somnul întâmplător se întâmplă în interiorul gorutinei pe care fiecare sumă()
funcția invocă.
Iată partea răcoroasă. Selectați
declarația permite principal()
funcția așteptați pe toate canalele și răspundeți la prima care se întoarce. Selectați
declarația funcționează puțin asemănător intrerupator
afirmație.
func () r: = rand.New (rand.NewSource (time.Now () UnixNano ())) suma: = func (a int, b int) <-chan int ch := make(chan int) go func() // Random time up to one second delay := time.Duration(r.Int()%1000) * time.Millisecond time.Sleep(delay) ch <- a + b close(ch) () return ch // Call sum 4 times with the same parameters ch1 := sum(3, 5) ch2 := sum(3, 5) ch3 := sum(3, 5) ch4 := sum(3, 5) // wait for the first goroutine to write to its channel select case result := <-ch1: fmt.Printf("ch1: 3 + 5 = %d", result) case result := <-ch2: fmt.Printf("ch2: 3 + 5 = %d", result) case result := <-ch3: fmt.Printf("ch3: 3 + 5 = %d", result) case result := <-ch4: fmt.Printf("ch4: 3 + 5 = %d", result)
Uneori nu vrei principal()
funcția de a bloca așteptarea chiar și pentru prima gorutină pentru a termina. În acest caz, puteți adăuga un caz implicit care va fi executat dacă toate canalele sunt blocate.
În articolul meu anterior, am arătat o soluție pentru exercițiul de navigare web de la Tour of Go. Am folosit gorutine și o hartă sincronizată. De asemenea, am rezolvat exercițiul folosind canalele. Codul sursă complet pentru ambele soluții este disponibil pe GitHub.
Să ne uităm la părțile relevante. Mai întâi, aici este un struct care va fi trimis la un canal ori de câte ori o gorutină analizează o pagină. Acesta conține adâncimea curentă și toate adresele URL găsite pe pagină.
tip link-uri struct urls [] string depth
fetchURL ()
funcția acceptă o adresă URL, o adâncime și un canal de ieșire. Utilizează încărcătorul (furnizat de exercițiu) pentru a obține adresele URL ale tuturor legăturilor din pagină. Trimite o listă de adrese URL ca un singur mesaj pe canalul candidatului ca o Link-uri
struct cu o adâncime descrescătoare. Adâncimea reprezintă cât de mult mai trebuie să ne accesăm cu crawlere. Când adâncimea atinge 0, nu trebuie să se efectueze o altă prelucrare.
funcția fetchURL (url șir, adâncimea int, candidații chan link-uri) body, urls, err: = fetcher.Fetch (url) fmt.Printf ("% s% q \ n" nil fmt.Println (err) candidați <- linksurls, depth - 1
ChannelCrawl ()
funcția coordonează totul. Acesta ține evidența tuturor adreselor URL care au fost deja încărcate pe o hartă. Nu este nevoie să sincronizați accesul deoarece nu atinge altă funcție sau gorutină. De asemenea, definește un canal candidat la care toate gorutinele își vor scrie rezultatele.
Apoi începe să invocă parseUrl
ca gorutine pentru fiecare nouă adresă URL. Logica ține evidența numărului de gorutine lansate prin gestionarea unui contor. Ori de câte ori se citește o valoare din canal, contorul este decrementat (deoarece gorutina trimisă iese după trimitere) și ori de câte ori este lansată o nouă gorutină, contorul este incrementat. Dacă adâncimea ajunge la zero, atunci nu vor fi lansate noi gorutine și funcția principală va continua să citească de la canal până când toate gorutinele sunt terminate.
// ChannelCrawl accesează cu crawlere link-urile de la o adresă URL pentru semințe func ChannelCrawl (șir url, adâncime int, fetcher Fetcher) candidates: = make (chan links, 0) preluat: = make (map [string] bool) counter: = 1 // Fetch url inițial pentru însămânțarea canalului de candidați merge fetchURL (url, adâncime, candidați) pentru contor> 0 candidateLinks: = <-candidates counter-- depth = candidateLinks.depth for _, candidate := range candidateLinks.urls // Already fetched. Continue… if fetched[candidate] continue // Add to fetched mapped fetched[candidate] = true if depth > 0 counter ++ go fetchURL (candidat, adâncime, candidați)
Canalele Go oferă o mulțime de opțiuni pentru o comunicare sigură între gorutine. Suportul pentru sintaxa este concis și ilustrativ. Este un avantaj real pentru exprimarea algoritmilor concurenți. Există mult mai multe canale decât am prezentat aici. Vă încurajez să vă scufundați și să vă familiarizați cu diferitele modele de concurență pe care le permit.