En av Goets unika egenskaper är användningen av kanaler för att kommunicera säkert mellan goroutiner. I den här artikeln lär du dig vilka kanaler som finns, hur du använder dem effektivt och några vanliga mönster.
En kanal är en synkroniserad in-memory-kö som goroutiner och vanliga funktioner kan använda för att skicka och ta emot typade värden. Kommunikation serialiseras via kanalen.
Du skapar en kanal med göra()
och ange vilken typ av värden kanalen accepterar:
ch: = gör (chan int)
Go ger en bra pilsyntax för att skicka och ta emot till / från kanaler:
// skicka värde till en kanal ch <- 5 // receive value from a channel x := <- ch
Du behöver inte konsumera värdet. Det är OK att bara dyka upp ett värde från en kanal:
<-ch
Kanaler blockerar som standard. Om du skickar ett värde till en kanal blockerar du tills någon tar emot den. På samma sätt kan du blockera tills någon skickar ett värde till kanalen om du tar emot en kanal.
Följande program visar detta. De main ()
funktionen gör en kanal och startar en rutin som kallas att utskrifter "startar", läser ett värde från kanalen och skriver också ut. Sedan main ()
startar en annan goroutine som bara skriver ut ett streck ("-") varje sekund. Sedan sover den i 2,5 sekunder, skickar ett värde till kanalen och sover 3 sekunder för att låta alla goroutiner sluta.
import ("fmt" "time") func main () ch: = make (chan int) // Starta en goroutine som läser ett värde från en kanal och skriver ut det går func (ch chan int) fmt.Println starta ") 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)
Detta program visar mycket bra kanalens blockerande natur. Den första goroutinen skriver "startar" genast, men blockeras då på försök att ta emot från kanalen fram till main ()
funktion, som sover i 2,5 sekunder och skickar värdet. Den andra goroutinen ger bara en visuell indikation av tidens flöde genom att skriva ut en streck regelbundet varje sekund.
Här är utgången:
starta - - 5 - - -
Detta beteende kopplar snabbt sändare till mottagare och ibland är inte det du vill ha. Go ger flera mekanismer för att hantera det.
Buffertkanaler är kanaler som kan hålla ett visst (fördefinierat) antal värden så att avsändarna inte blockerar tills bufferten är full, även om ingen tar emot.
För att skapa en buffrad kanal, lägg bara till en kapacitet som ett andra argument:
ch: = gör (chan int, 5)
Följande program illustrerar beteendet hos buffrade kanaler. De main ()
programmet definierar en buffrad kanal med en kapacitet på 3. Då startar den en goroutine som läser en buffert från kanalen varje sekund och skriver ut, och en annan goroutine som bara skriver ut en streck varje sekund för att ge en visuell indikation av tidens framåtskridande. Sedan skickar den fem värden till kanalen.
importera ("fmt" "tid") func main () ch: = make (chan int, 3) // Starta en goroutine som läser ett värde från kanalen varje sekund och skriver ut det går func (ch chan int) for time.Sleep (time.Second) fmt.Printf ("Goroutine fick:% 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)
Vad händer vid körning? De första tre värdena buffras av kanalen omedelbart och main ()
funktionsblock. Efter en sekund mottas ett värde av goroutinen och main ()
funktionen kan trycka ett annat värde. En annan sekund fortsätter, goroutinen får ett annat värde, och main ()
funktionen kan trycka på det sista värdet. Vid denna tidpunkt håller goroutinen emot värden från kanalen varje sekund.
Här är utgången:
Huvud () tryckt: 0 Huvud () tryckt: 1 Huvud () tryckt: 2 - Goroutine mottagen: 0 Huvud () tryckt: 3 - Goroutine mottagen: 1 Huvud () tryckt: 4 - Goroutine mottagen: 2 - Goroutine mottagen: 3 - Goroutine fick: 4
Buffertkanaler (så länge bufferten är tillräckligt stor) kan ta upp frågan om tillfälliga fluktuationer där det inte finns tillräckligt med mottagare för att behandla alla skickade meddelanden. Men det finns också det motsatta problemet med blockerade mottagare som väntar på att meddelanden ska behandlas. Go har fått dig täckt.
Vad händer om du vill att din goroutine ska göra något annat när det inte finns några meddelanden att behandla i en kanal? Ett bra exempel är om din mottagare väntar på meddelanden från flera kanaler. Du vill inte blockera på kanal A om kanal B har meddelanden just nu. Följande program försöker beräkna summan av 3 och 5 med maskinens fulla effekt.
Tanken är att simulera en komplex operation (t.ex. en fjärrfråga till en distribuerad DB) med redundans. De summa()
funktion (notera hur det definieras som nestad funktion inuti main ()
) accepterar två int parametrar och returnerar en int kanal. En intern anonym goroutine sover några slumpmässiga tid upp till en sekund och skriver sedan summan till kanalen, stänger den och returnerar den.
Nu, huvudsamtal summa (3, 5)
fyra gånger och lagrar de resulterande kanalerna i variablerna ch1 till ch4. De fyra ringer till summa()
återvända omedelbart eftersom slumpmässig sömn händer inom goroutin som varje summa()
funktionen påkallar.
Här kommer den snygga delen. De Välj
uttalande låter main ()
funktionen vänta på alla kanaler och svara på den första som kommer tillbaka. De Välj
uttalandet fungerar lite som växla
påstående.
func main () r: = rand.New (rand.NewSource (time.Now (). UnixNano ())) summa: = 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)
Ibland vill du inte ha main ()
funktion att blockera väntar även för att den första goroutinen ska slutföras. I det här fallet kan du lägga till ett standardfall som ska utföras om alla kanaler är blockerade.
I min tidigare artikel visade jag en lösning på webbrobotövningen från Tour of Go. Jag har använt goroutiner och en synkroniserad karta. Jag löste också övningen med hjälp av kanaler. Den kompletta källkoden för båda lösningarna finns tillgänglig på GitHub.
Låt oss titta på relevanta delar. Först här är en struktur som kommer att skickas till en kanal när en goroutine analyserar en sida. Den innehåller det aktuella djupet och alla webbadresser som hittades på sidan.
skriv länkar struct urls [] strängdjup int
De fetchURL ()
funktionen accepterar en URL, ett djup och en utmatningskanal. Den använder fetcher (tillhandahålls av övningen) för att få webbadresserna till alla länkar på sidan. Den skickar listan över webbadresser som ett enda meddelande till kandidatens kanal som en länkar
struct med ett minskat djup. Djupet representerar hur mycket längre vi ska krypa. När djupet når 0, bör ingen ytterligare bearbetning ske.
func fetchURL (url sträng, djup int, kandidater chan länkar) body, urls, err: = fetcher.Fetch (url) fmt.Printf ("hittade:% s% q \ n", url, kropp) om err! = nil fmt.Println (err) kandidater <- linksurls, depth - 1
De ChannelCrawl ()
funktionen koordinerar allt. Det håller reda på alla webbadresser som redan hämtats i en karta. Det finns inget behov av att synkronisera åtkomst eftersom ingen annan funktion eller goroutine röra. Det definierar också en kandidatkanal som alla goroutiner kommer att skriva sina resultat på.
Sedan börjar det åberopa parseUrl
som goroutiner för varje ny URL. Logiken håller reda på hur många goroutiner som lanserades genom att hantera en räknare. När ett värde läses från kanalen minskas räknaren (eftersom den sändande goroutinen lämnar ut efter att ha skickats), och när en ny goroutin lanseras ökas räknaren. Om djupet blir noll så kommer inga nya goroutiner att lanseras, och huvudfunktionen fortsätter att läsa från kanalen tills alla goroutiner är färdiga.
// ChannelCrawl crawls länkar från en seed url func ChannelCrawl (url sträng, djup int, fetcher Fetcher) kandidater: = gör (chan länkar, 0) hämtade: = gör (map [string] bool) counter: = 1 // Hämta första url till frö kandidatens kanal går fetchURL (url, djup, kandidater) för räknare> 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, djup, kandidater)
Go-kanalerna ger många alternativ för säker kommunikation mellan goroutiner. Syntaxstödet är både kortfattat och illustrativt. Det är en riktig välsignelse för att uttrycka samtidiga algoritmer. Det finns mycket mer att kanaler än jag presenterade här. Jag uppmuntrar dig att dyka in och bli bekant med de olika samtidiga mönster som de möjliggör.