Andiamo Golang Concurrency, parte 2

Panoramica

Una delle caratteristiche uniche di Go è l'uso di canali per comunicare in sicurezza tra le goroutine. In questo articolo, imparerai quali sono i canali, come usarli in modo efficace e alcuni modelli comuni. 

Cos'è un canale?

Un canale è una coda in memoria sincronizzata che goreutine e funzioni regolari possono utilizzare per inviare e ricevere valori digitati. La comunicazione è serializzata attraverso il canale.

Si crea un canale usando rendere() e specifica il tipo di valori accettati dal canale:

ch: = make (chan int)

Go fornisce una bella sintassi delle frecce per l'invio e la ricezione da / verso i canali:

 // invia un valore a un canale ch <- 5 // receive value from a channel x := <- ch

Non devi consumare il valore. Va bene solo per visualizzare un valore da un canale:

<-ch

I canali stanno bloccando per impostazione predefinita. Se invii un valore a un canale, blocchi fino a quando qualcuno non lo riceve. Allo stesso modo, se ricevi da un canale, bloccherai fino a quando qualcuno non invierà un valore al canale.  

Il seguente programma lo dimostra. Il principale() la funzione crea un canale e avvia una routine di go, chiamata "stampa", legge un valore dal canale e stampa anche. Poi principale() avvia un'altra goroutine che stampa un trattino ("-") ogni secondo. Quindi, dorme per 2,5 secondi, invia un valore al canale e dorme altri 3 secondi per far terminare tutte le goroutine.

import ("fmt" "time") func main () ch: = make (chan int) // Avvia una goroutine che legge un valore da un canale e lo stampa go 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) 

Questo programma dimostra molto bene la natura di blocco del canale. La prima goroutine stampa immediatamente "start", ma poi viene bloccata nel tentativo di ricevere dal canale fino al principale() funzione, che dorme per 2,5 secondi e invia il valore. L'altra goroutine fornisce solo un'indicazione visiva del flusso del tempo stampando un trattino regolarmente ogni secondo. 

Ecco l'output:

inizio - - 5 - - -

Canali bufferizzati

Questo comportamento accoppia strettamente i mittenti ai ricevitori e talvolta non è quello che desideri. Go fornisce diversi meccanismi per affrontarlo.

I canali bufferizzati sono canali che possono contenere un certo numero (predefinito) di valori in modo che i mittenti non blocchino fino a quando il buffer non è pieno, anche se nessuno sta ricevendo. 

Per creare un canale bufferizzato, aggiungi una capacità come secondo argomento:

ch: = make (chan int, 5)

Il seguente programma illustra il comportamento dei canali bufferizzati. Il principale() programma definisce un canale bufferizzato con una capacità di 3. Quindi avvia una goroutine che legge un buffer dal canale ogni secondo e stampa, e un'altra goroutine che stampa solo un trattino ogni secondo per dare un'indicazione visiva del progresso del tempo. Quindi, invia cinque valori al canale. 

import ("fmt" "time") func main () ch: = make (chan int, 3) // Avvia una goroutine che legge un valore dal canale ogni secondo e lo stampa go func (ch chan int) per time.Sleep (time.Second) fmt.Printf ("Goroutine ricevuta:% 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) 

Cosa succede al runtime? I primi tre valori vengono bufferati dal canale immediatamente e il principale() blocchi funzionali. Dopo un secondo, un valore viene ricevuto dalla goroutine, e il principale() la funzione può spingere un altro valore. Un altro secondo passa, la goroutine riceve un altro valore, e il principale() la funzione può spingere l'ultimo valore. A questo punto, la goroutine continua a ricevere valori dal canale ogni secondo. 

Ecco l'output:

main () premuto: 0 main () premuto: 1 main () premuto: 2 - Goroutine ricevuto: 0 main () premuto: 3 - Goroutine ricevuto: 1 main () premuto: 4 - Goroutine ricevuto: 2 - Goroutine ricevuto: 3 - Goroutine ricevuta: 4

Selezionare

I canali bufferizzati (a condizione che il buffer sia abbastanza grande) possono risolvere il problema delle fluttuazioni temporanee in cui non ci sono abbastanza ricevitori per elaborare tutti i messaggi inviati. Ma c'è anche il problema opposto dei ricevitori bloccati in attesa che i messaggi vengano elaborati. Go ti ha coperto. 

Cosa succede se vuoi che la tua goroutine faccia qualcos'altro quando non ci sono messaggi da elaborare in un canale? Un buon esempio è se il tuo ricevitore è in attesa di messaggi da più canali. Non vuoi bloccare sul canale A se il canale B ha messaggi in questo momento. Il seguente programma tenta di calcolare la somma di 3 e 5 utilizzando tutta la potenza della macchina. 

L'idea è di simulare un'operazione complessa (ad esempio una query remota a un DB distribuito) con ridondanza. Il somma() funzione (nota come è definita come funzione annidata all'interno principale()) accetta due parametri int e restituisce un canale int. Una goroutine anonima interna dorme un po 'di tempo casuale fino ad un secondo e poi scrive la somma sul canale, la chiude e la restituisce.

Ora, le chiamate principali somma (3, 5) quattro volte e memorizza i canali risultanti nelle variabili da ch1 a ch4. Le quattro chiamate a somma() ritorna immediatamente perché il sonno casuale avviene all'interno della goroutine di ciascuno somma() la funzione invoca.

Ecco la parte interessante. Il selezionare la dichiarazione lascia il principale() la funzione attende su tutti i canali e risponde al primo che ritorna. Il selezionare la dichiarazione funziona un po 'come il interruttore dichiarazione.

func main () r: = rand.New (rand.NewSource (time.Now (). UnixNano ())) sum: = 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)  

A volte non vuoi il principale() funzione per bloccare l'attesa anche alla fine della prima goroutine. In questo caso, puoi aggiungere un caso predefinito che verrà eseguito se tutti i canali sono bloccati.

Un esempio di Web Crawler

Nel mio precedente articolo, ho mostrato una soluzione per l'esercizio del crawler web dal Tour of Go. Ho usato le goroutine e una mappa sincronizzata. Ho anche risolto l'esercizio utilizzando i canali. Il codice sorgente completo per entrambe le soluzioni è disponibile su GitHub.

Diamo un'occhiata alle parti rilevanti. Innanzitutto, ecco una struttura che verrà inviata a un canale ogni volta che una goroutine analizza una pagina. Contiene la profondità attuale e tutti gli URL trovati nella pagina.

digita link struct urls [] string depth int

Il FetchUrl () la funzione accetta un URL, una profondità e un canale di uscita. Usa il fetcher (fornito dall'esercitazione) per ottenere gli URL di tutti i link sulla pagina. Invia l'elenco di URL come un singolo messaggio al canale del candidato come a link strutturare con una profondità decrementata. La profondità rappresenta quanto ancora dovremmo strisciare. Quando la profondità raggiunge 0, non dovrebbe avvenire alcuna ulteriore elaborazione.

func fetchURL (url string, depth int, candidati chan links) body, urls, err: = fetcher.Fetch (url) fmt.Printf ("trovato:% s% q \ n", url, body) se err! = nil fmt.Println (err) candidati <- linksurls, depth - 1 

Il ChannelCrawl () la funzione coordina tutto Tiene traccia di tutti gli URL che sono stati già recuperati in una mappa. Non è necessario sincronizzare l'accesso perché nessuna altra funzione o goroutine è in contatto. Definisce anche un canale candidato a cui tutte le goroutine scriveranno i loro risultati.

Quindi, inizia a invocare parseURL come goroutine per ogni nuovo URL. La logica tiene traccia di quante goroutine sono state lanciate gestendo un contatore. Ogni volta che un valore viene letto dal canale, il contatore viene decrementato (poiché la goroutine di invio esce dopo l'invio) e ogni volta che viene lanciata una nuova goroutine, il contatore viene incrementato. Se la profondità arriva a zero, non verranno lanciate nuove goroutine e la funzione principale continuerà a leggere dal canale fino a quando tutte le goroutine non saranno completate.

// ChannelCrawl esegue la scansione dei collegamenti da una funzione url seme ChannelCrawl (stringa url, profondità int, fetcher Fetcher) candidati: = make (collegamenti chan, 0) recuperati: = make (mappa [stringa] bool) contatore: = 1 // Fetch url iniziale per inizializzare il canale candidati go fetchURL (url, depth, candidati) per counter> 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 (candidato, profondità, candidati)

Conclusione

I canali di Go offrono molte opzioni per una comunicazione sicura tra le goroutine. Il supporto della sintassi è sia conciso che illustrativo. È un vero vantaggio per esprimere algoritmi concorrenti. C'è molto di più per i canali di quello che ho presentato qui. Vi incoraggio ad immergervi e familiarizzare con i vari modelli di concorrenza che abilitano.