Een van de unieke kenmerken van Go is het gebruik van kanalen om veilig te communiceren tussen goroutines. In dit artikel leert u welke kanalen zijn, hoe u ze effectief kunt gebruiken en wat algemene patronen.
Een kanaal is een gesynchroniseerde wachtrij in het geheugen die goroutines en reguliere functies kunnen gebruiken om getypte waarden te verzenden en ontvangen. Communicatie wordt geserialiseerd via het kanaal.
Je maakt een kanaal met behulp van maken()
en geef het type waarden op dat het kanaal accepteert:
ch: = make (chan int)
Go biedt een mooie pijlsyntaxis voor het verzenden en ontvangen van / naar kanalen:
// stuur waarde naar een kanaal-ch <- 5 // receive value from a channel x := <- ch
Je hoeft de waarde niet te consumeren. Het is OK om een waarde uit een kanaal te laten zien:
<-ch
Kanalen blokkeren standaard. Als u een waarde naar een kanaal verzendt, blokkeert u totdat iemand het ontvangt. Op dezelfde manier, als je van een kanaal ontvangt, blokkeer je totdat iemand een waarde naar het kanaal zendt.
Het volgende programma laat dit zien. De hoofd()
functie maakt een kanaal en start een go-routine genaamd "start", leest een waarde uit het kanaal en drukt ook af. Dan hoofd()
start een andere goroutine die elke seconde slechts een streepje ("-") afdrukt. Daarna slaapt het gedurende 2,5 seconde, stuurt het een kanaal naar het kanaal en slaapt nog 3 seconden om alle gorotines te laten eindigen.
import ("fmt" "time") func main () ch: = make (chan int) // Start een goroutine die een waarde van een kanaal leest en print deze 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)
Dit programma demonstreert heel goed de blokkerende aard van het kanaal. De eerste goroutine print meteen "start", maar wordt dan geblokkeerd bij pogingen om van het kanaal te ontvangen tot de hoofd()
functie, die 2,5 seconde slaapt en de waarde verzendt. De andere goroutine geeft slechts een visuele indicatie van de tijdstroom door elke seconde een streepje regelmatig af te drukken.
Dit is de uitvoer:
start - - 5 - - -
Dit gedrag koppelt afzenders aan ontvangers en is soms niet wat u zoekt. Go biedt verschillende mechanismen om dat aan te pakken.
Gebufferde kanalen zijn kanalen die een bepaald (vooraf gedefinieerd) aantal waarden kunnen bevatten, zodat afzenders niet blokkeren totdat de buffer vol is, zelfs als niemand ontvangt.
Als u een gebufferd kanaal wilt maken, voegt u een capaciteit als een tweede argument toe:
ch: = make (chan int, 5)
Het volgende programma illustreert het gedrag van gebufferde kanalen. De hoofd()
programma definieert een gebufferd kanaal met een capaciteit van 3. Dan start het een goroutine die elke seconde een buffer uit het kanaal leest en drukt, en een andere goroutine die elke seconde een streepje afdrukt om een visuele indicatie van de voortgang van de tijd te geven. Vervolgens worden er vijf waarden naar het kanaal verzonden.
import ("fmt" "time") func main () ch: = make (chan int, 3) // Start een goroutine die elke seconde een waarde uit het kanaal leest en print deze go func (ch chan int) for time.Sleep (time.Second) fmt.Printf ("Goroutine received:% 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)
Wat gebeurt er tijdens runtime? De eerste drie waarden worden onmiddellijk door het kanaal gebufferd, en de hoofd()
functieblokken. Na een seconde wordt een waarde ontvangen door de goroutine en de hoofd()
functie kan een andere waarde pushen. Een andere seconde gaat voorbij, de goroutine krijgt een andere waarde, en de hoofd()
functie kan de laatste waarde pushen. Op dit punt blijft de goroutine elke seconde waarden ontvangen van het kanaal.
Dit is de uitvoer:
main () geduwd: 0 main () pushed: 1 main () pushed: 2 - Goroutine ontvangen: 0 main () pushed: 3 - Goroutine ontvangen: 1 main () pushed: 4 - Goroutine ontvangen: 2 - Goroutine ontvangen: 3 - Goroutine ontvangen: 4
Gebufferde kanalen (zolang de buffer groot genoeg is) kunnen het probleem van tijdelijke fluctuaties aanpakken waarbij er niet voldoende ontvangers zijn om alle verzonden berichten te verwerken. Maar er is ook het tegenovergestelde probleem van geblokkeerde ontvangers die wachten op de verwerking van berichten. Go heeft jou gedekt.
Wat als u wilt dat uw goroutine iets anders doet als er geen berichten in een kanaal verwerkt moeten worden? Een goed voorbeeld is als uw ontvanger wacht op berichten van meerdere kanalen. Je wilt niet blokkeren op kanaal A als kanaal B op dit moment berichten heeft. Het volgende programma probeert de som van 3 en 5 te berekenen met de volledige kracht van de machine.
Het idee is om een complexe bewerking (bijvoorbeeld een query op afstand naar een gedistribueerde database) te simuleren met redundantie. De som()
functie (merk op hoe het binnenin als geneste functie is gedefinieerd hoofd()
) accepteert twee int-parameters en retourneert een int-kanaal. Een interne anonieme goroutine slaapt een willekeurige tijd tot een seconde en schrijft vervolgens de som naar het kanaal, sluit deze en retourneert deze.
Nu, hoofdoproepen som (3, 5)
vier keer en slaat de resulterende kanalen op in variabelen ch1 tot ch4. De vier oproepen naar som()
terugkeer onmiddellijk omdat de willekeurige slaap gebeurt binnen de goroutine die elk som()
functie roept aan.
Hier komt het coole deel. De kiezen
verklaring laat de hoofd()
functie wacht op alle kanalen en reageer op de eerste die terugkeert. De kiezen
verklaring werkt een beetje zoals de schakelaar
uitspraak.
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)
Soms wil je het niet hoofd()
functie om wachten te blokkeren, zelfs als de eerste goroutine is voltooid. In dit geval kunt u een standaardcase toevoegen die wordt uitgevoerd als alle kanalen zijn geblokkeerd.
In mijn vorige artikel toonde ik een oplossing voor de webcrawler-oefening van de Tour of Go. Ik heb goroutines en een gesynchroniseerde kaart gebruikt. Ik heb ook de oefening met behulp van kanalen opgelost. De volledige broncode voor beide oplossingen is beschikbaar op GitHub.
Laten we naar de relevante delen kijken. Ten eerste, hier is een struct die naar een kanaal wordt verzonden wanneer een goroutine een pagina parseert. Het bevat de huidige diepte en alle URL's op de pagina.
type links struct urls [] string diepte int
De fetchURL ()
functie accepteert een URL, een diepte en een uitvoerkanaal. Het gebruikt de fetcher (geleverd door de oefening) om de URL's van alle links op de pagina te krijgen. Het stuurt de lijst met URL's als een enkel bericht naar het kanaal van de kandidaat als een koppelingen
struct met een verminderde diepte. De diepte geeft aan hoeveel verder we moeten kruipen. Wanneer de diepte 0 bereikt, zou er geen verdere verwerking moeten plaatsvinden.
func fetchURL (url string, diepte int, kandidaten chan links) body, urls, err: = fetcher.Fetch (url) fmt.Printf ("gevonden:% s% q \ n", url, body) if error! = nihil fmt.Println (err) kandidaten <- linksurls, depth - 1
De ChannelCrawl ()
functie coördineert alles. Het houdt alle URL's bij die al op een kaart zijn opgehaald. Het is niet nodig om de toegang te synchroniseren omdat geen andere functie of goroutine elkaar aanraakt. Het definieert ook een kandidaatkanaal waar alle goroutines hun resultaten naar schrijven.
Vervolgens begint het aan te roepen parseUrl
als goroutines voor elke nieuwe URL. De logica houdt bij hoeveel goroutines werden gelanceerd door een teller te beheren. Telkens wanneer een waarde van het kanaal wordt gelezen, wordt de teller verlaagd (omdat de verzendende goroutine na verzending wordt verlaten) en wanneer een nieuwe goroutine wordt gelanceerd, wordt de teller verhoogd. Als de diepte op nul komt, worden er geen nieuwe goroutines gelanceerd en blijft de hoofdfunctie van het kanaal af totdat alle goroutines zijn voltooid.
// ChannelCrawl crawlt koppelingen vanuit een seed-URL-func ChannelCrawl (url-tekenreeks, diepte int, fetcher Fetcher) kandidaten: = make (chan-links, 0) opgehaald: = make (map [string] bool) -teller: = 1 // Fetch initiële URL om het kanaal van de kandidaat te seeden go fetchURL (url, diepte, kandidaten) voor teller> 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 (kandidaat, diepte, kandidaten)
Go's kanalen bieden veel opties voor veilige communicatie tussen goroutines. De syntaxisondersteuning is zowel beknopt als illustratief. Het is een echte zegen voor het uitdrukken van gelijktijdige algoritmen. Er is veel meer aan kanalen dan ik hier presenteerde. Ik moedig je aan om een duik te nemen in en vertrouwd te raken met de verschillende gelijktijdigheidspatronen die ze mogelijk maken.