En av de unike egenskapene til Go er bruken av kanaler for å kommunisere trygt mellom goroutiner. I denne artikkelen lærer du hvilke kanaler som er, hvordan du bruker dem effektivt, og noen vanlige mønstre.
En kanal er en synkronisert minnekø som goroutiner og vanlige funksjoner kan bruke til å sende og motta typede verdier. Kommunikasjon er serialisert gjennom kanalen.
Du lager en kanal ved hjelp av gjøre()
og angi hvilken type verdier kanalen aksepterer:
ch: = lage (chan int)
Go gir en fin pilsyntax for sending og mottak til / fra kanaler:
// send verdi til en kanal ch <- 5 // receive value from a channel x := <- ch
Du trenger ikke å forbruke verdien. Det er ok bare å dukke opp en verdi fra en kanal:
<-ch
Kanaler blokkerer som standard. Hvis du sender en verdi til en kanal, vil du blokkere inntil noen mottar den. På samme måte, hvis du mottar fra en kanal, vil du blokkere til noen sender en verdi til kanalen.
Følgende program demonstrerer dette. De hoved()
funksjonen gjør en kanal og starter en rutine som kalles at utskrifter "start", leser en verdi fra kanalen og skriver også ut. Deretter hoved()
starter en annen goroutine som bare skriver ut et dash ("-") hvert sekund. Deretter sover den i 2,5 sekunder, sender en verdi til kanalen og sover 3 flere sekunder for å la alle goroutinerne avsluttes.
import ("fmt" "time") func main () ch: = lage (chan int) // Start en goroutine som leser en verdi fra en kanal og skriver ut den går 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)
Dette programmet demonstrerer veldig bra kanalens blokkering. Den første goroutinen skriver "start" med en gang, men da er den blokkert på å prøve å motta fra kanalen til hoved()
funksjon, som sover i 2,5 sekunder og sender verdien. Den andre goroutinen gir bare en visuell indikasjon på tidens strømning ved å skrive ut et dash regelmessig hvert sekund.
Her er utgangen:
start - - 5 - - -
Denne oppførselen tetter par sendere til mottakere og noen ganger er ikke det du vil ha. Go gir flere mekanismer for å adressere det.
Buffered kanaler er kanaler som kan holde et bestemt (forhåndsdefinert) antall verdier, slik at avsendere ikke blokkerer til bufferen er full, selv om ingen mottar.
For å opprette en buffret kanal, legg bare til en kapasitet som et annet argument:
ch: = lage (chan int, 5)
Følgende program illustrerer oppførselen til buffrede kanaler. De hoved()
Programmet definerer en bufret kanal med en kapasitet på 3. Da starter den en goroutine som leser en buffer fra kanalen hvert sekund og skriver ut, og en annen goroutine som bare skriver ut et dash hvert sekund for å gi en visuell indikasjon på tidens fremgang. Deretter sender den fem verdier til kanalen.
import ("fmt" "tid") func main () ch: = lage (chan int, 3) // Start en goroutine som leser en verdi fra kanalen hvert sekund og skriver ut det går func (ch chan int) for time.Sleep (time.Second) fmt.Printf ("Goroutine mottatt:% 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)
Hva skjer ved kjøretid? De tre første verdiene er bufret av kanalen umiddelbart, og hoved()
funksjonsblokker. Etter et sekund mottas en verdi av goroutinen, og hoved()
funksjonen kan trykke en annen verdi. Et annet sekund går forbi, goroutinen mottar en annen verdi, og hoved()
funksjonen kan trykke på den siste verdien. På dette tidspunktet fortsetter goroutinen å motta verdier fra kanalen hvert sekund.
Her er utgangen:
main () presset: 0 main () presset: 1 main () presset: 2 - Goroutine mottatt: 0 main () presset: 3 - Goroutine mottatt: 1 main () presset: 4 - Goroutine mottatt: 2 - Goroutine mottatt: 3 - Goroutine mottok: 4
Buffered kanaler (så lenge bufferen er stor nok) kan løse problemet med midlertidige svingninger der det ikke er nok mottakere til å behandle alle sendte meldinger. Men det er også det motsatte problemet med blokkerte mottakere som venter på meldinger som skal behandles. Go har fått deg dekket.
Hva om du vil at goroutinen skal gjøre noe annet når det ikke er noen meldinger å behandle i en kanal? Et godt eksempel er at mottakeren venter på meldinger fra flere kanaler. Du vil ikke blokkere på kanal A hvis kanal B har meldinger akkurat nå. Følgende program forsøker å beregne summen av 3 og 5 ved hjelp av maskinens fullstendige effekt.
Tanken er å simulere en komplisert operasjon (for eksempel en ekstern forespørsel til en distribuert DB) med redundans. De sum()
funksjon (merk hvordan den er definert som nestet funksjon inni hoved()
) aksepterer to int parametere og returnerer en int kanal. En intern anonym goroutine sover litt tilfeldig tid opp til ett sekund og skriver deretter summen til kanalen, lukker den og returnerer den.
Nå, hovedsamtaler summen (3, 5)
fire ganger og lagrer de resulterende kanalene i variabler ch1 til ch4. De fire ringer til sum()
returnere umiddelbart fordi tilfeldig søvn skjer i goroutinen som hver sum()
funksjonen påkaller.
Her kommer den kule delen. De å velge
uttalelse lar hoved()
funksjon vent på alle kanaler og svar på den første som kommer tilbake. De å velge
erklæring fungerer litt som bytte om
uttalelse.
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)
Noen ganger vil du ikke ha hoved()
funksjon for å blokkere venter selv om den første goroutinen er ferdig. I dette tilfellet kan du legge til et standard tilfelle som vil utføres hvis alle kanalene er blokkert.
I min forrige artikkel viste jeg en løsning på web-crawlerøvelsen fra Tour of Go. Jeg har brukt goroutiner og et synkronisert kart. Jeg løste også øvelsen ved hjelp av kanaler. Den komplette kildekoden for begge løsningene er tilgjengelig på GitHub.
La oss se på de aktuelle delene. Først, her er en struktur som vil bli sendt til en kanal når en goroutine analyserer en side. Den inneholder nåværende dybde og alle nettadresser som er funnet på siden.
skriv koblinger struct urls [] strengdybde int
De fetchURL ()
funksjonen godtar en nettadresse, en dybde og en utgangskanal. Den bruker fetcher (levert av øvelsen) for å få nettadressene til alle linkene på siden. Den sender listen over nettadresser som en enkelt melding til kandidatens kanal som en lenker
struktur med en dekrementert dybde. Dybden representerer hvor mye lenger skal vi krype. Når dybden når 0, bør det ikke foretas ytterligere behandling.
func fetchURL (url streng, dybde int, kandidater chan lenker) body, urls, err: = fetcher.Fetch (url) fmt.Printf ("funnet:% s% q \ n", url, kropp) hvis feil! = null fmt.Println (err) kandidater <- linksurls, depth - 1
De ChannelCrawl ()
funksjonen koordinerer alt. Den holder styr på alle nettadressene som allerede ble hentet i et kart. Det er ikke nødvendig å synkronisere tilgang fordi ingen annen funksjon eller goroutin berører. Det definerer også en kandidatkanal som alle goroutinene vil skrive sine resultater til.
Så begynner det å påkalle seg parseUrl
som goroutiner for hver ny nettadresse. Logikken holder styr på hvor mange goroutiner ble lansert ved å administrere en teller. Når en verdi leses fra kanalen, blir telleren redusert (fordi den sendte goroutinen kommer ut etter sending), og når en ny goroutin blir lansert, økes telleren. Hvis dybden kommer til null, vil ingen nye goroutiner bli lansert, og hovedfunksjonen vil fortsette å lese fra kanalen til alle goroutiner er ferdige.
// ChannelCrawl gjennomsøker lenker fra en seed url func ChannelCrawl (url streng, dybde int, fetcher Fetcher) kandidater: = lage (chan links, 0) hentet: = lage (map [string] bool) counter: = 1 // Hent innledende url til frø kandidatkanalen gå fetchURL (url, dybde, kandidater) for 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 (kandidat, dybde, kandidater)
Go's kanaler gir mange muligheter for sikker kommunikasjon mellom goroutiner. Syntaxstøtten er både kortfattet og illustrerende. Det er en ekte velsignelse for å uttrykke samtidige algoritmer. Det er mye mer til kanaler enn jeg presenterte her. Jeg oppfordrer deg til å dykke inn og bli kjent med de ulike samtidighetsmønstrene de aktiverer.