Allons-y concurrence de Golang, partie 2

Vue d'ensemble

L'une des caractéristiques uniques de Go est l'utilisation de canaux pour communiquer en toute sécurité entre goroutines. Dans cet article, vous apprendrez ce que sont les canaux, comment les utiliser efficacement et quelques modèles courants.. 

Qu'est-ce qu'un canal??

Un canal est une file d'attente en mémoire synchronisée que les goroutines et les fonctions standard peuvent utiliser pour envoyer et recevoir des valeurs typées. La communication est sérialisée via le canal.

Vous créez un canal en utilisant faire() et spécifiez le type de valeurs que le canal accepte:

ch: = make (chan int)

Go fournit une belle syntaxe de flèche pour envoyer et recevoir des canaux:

 // envoie une valeur à un canal ch <- 5 // receive value from a channel x := <- ch

Vous n'êtes pas obligé de consommer la valeur. C'est juste pour faire apparaître une valeur d'un canal:

<-ch

Les canaux bloquent par défaut. Si vous envoyez une valeur à un canal, vous bloquerez jusqu'à ce que quelqu'un la reçoive. De même, si vous recevez un canal, vous bloquez jusqu'à ce que quelqu'un envoie une valeur au canal..  

Le programme suivant le montre. le principale() function crée un canal et lance une routine de lecture appelée "start", lit une valeur sur le canal et imprime aussi. ensuite principale() démarre un autre goroutine qui imprime simplement un tiret ("-") toutes les secondes. Ensuite, il dort pendant 2,5 secondes, envoie une valeur au canal et dort 3 secondes de plus pour laisser tous les goroutines finir.

import ("fmt" "time") func main () ch: = make (chan int) // Lance un goroutine qui lit une valeur sur un canal et l’imprime aller 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) 

Ce programme démontre très bien la nature bloquante de la chaîne. Le premier goroutine imprime "commence" tout de suite, mais est bloqué lorsqu’il essaie de recevoir du canal jusqu’à ce que le principale() fonction, qui dort pendant 2,5 secondes et envoie la valeur. L’autre goroutine fournit simplement une indication visuelle de l’écoulement du temps en imprimant un tiret régulièrement toutes les secondes.. 

Voici la sortie:

commencer - - 5 - - -

Canaux tamponnés

Ce comportement associe étroitement les expéditeurs et les destinataires et n’est parfois pas ce que vous souhaitez. Go fournit plusieurs mécanismes pour résoudre ce problème.

Les canaux en mémoire tampon sont des canaux pouvant contenir un certain nombre de valeurs (prédéfinies) afin que les expéditeurs ne se bloquent pas tant que la mémoire tampon n'est pas pleine, même si personne ne les reçoit. 

Pour créer un canal en mémoire tampon, ajoutez simplement une capacité en tant que second argument:

ch: = make (chan int, 5)

Le programme suivant illustre le comportement des canaux mis en mémoire tampon. le principale() programme définit un canal mis en mémoire tampon d'une capacité de 3. Ensuite, il commence un goroutine qui lit un tampon dans le canal toutes les secondes et l'imprime, et un autre goroutine qui n'imprime qu'un trait par seconde pour donner une indication visuelle de la progression du temps. Ensuite, il envoie cinq valeurs au canal. 

import ("fmt" "time") func main () ch: = make (chan int, 3) // Lance un goroutine qui lit une valeur du canal toutes les secondes et l’imprime aller func (ch chan int) for time.Sleep (time.Second) fmt.Printf ("Goroutine reçu:% 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) 

Que se passe-t-il à l'exécution? Les trois premières valeurs sont immédiatement mises en tampon par le canal, et le principale() blocs de fonction. Après une seconde, une valeur est reçue par la goroutine, et le principale() fonction peut pousser une autre valeur. Une autre seconde passe, le goroutine reçoit une autre valeur, et le principale() fonction peut pousser la dernière valeur. À ce stade, le goroutine continue à recevoir des valeurs du canal toutes les secondes. 

Voici la sortie:

principal () poussé: 0 principal () poussé: 1 principal () poussé: 2 - Goroutine reçu: 0 principal () poussé: 3 - Goroutine reçu: 1 principal () poussé: 4 - Goroutine reçu: 2 - Goroutine reçu: 3 - Goroutine reçu: 4

Sélectionner

Les canaux mis en mémoire tampon (tant que la mémoire tampon est suffisamment grande) peuvent résoudre le problème des fluctuations temporaires en l'absence de récepteurs suffisants pour traiter tous les messages envoyés. Mais il y a aussi le problème opposé des destinataires bloqués en attente du traitement des messages. Va t'a couvert. 

Et si vous voulez que votre goroutine fasse autre chose quand il n'y a pas de messages à traiter dans un canal? Un bon exemple est si votre récepteur attend des messages de plusieurs canaux. Vous ne voulez pas bloquer sur le canal A si le canal B contient des messages pour le moment. Le programme suivant tente de calculer la somme de 3 et 5 en utilisant toute la puissance de la machine. 

L’idée est de simuler une opération complexe (par exemple une requête à distance vers une base de données distribuée) avec redondance. le somme() fonction (notez comment elle est définie comme fonction imbriquée dans principale()) accepte deux paramètres int et retourne un canal int. Un goroutine anonyme interne dort une durée aléatoire d'une seconde, puis écrit la somme sur le canal, la ferme et la renvoie..

Maintenant, les appels principaux somme (3, 5) quatre fois et stocke les canaux résultants dans les variables ch1 à ch4. Les quatre appels à somme() revenir immédiatement parce que le sommeil aléatoire se produit à l'intérieur de la goroutine somme() la fonction appelle.

Voici la partie cool. le sélectionner déclaration laisse le principale() fonction attend sur tous les canaux et répond au premier qui revient. le sélectionner déclaration fonctionne un peu comme le commutateur déclaration.

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)  

Parfois, vous ne voulez pas le principale() Fonction pour bloquer l'attente même lorsque la première goroutine est terminée. Dans ce cas, vous pouvez ajouter un cas par défaut qui sera exécuté si tous les canaux sont bloqués..

Un exemple Web Crawler

Dans mon article précédent, j’avais montré une solution à l’exercice relatif aux robots Web du Tour of Go. J'ai utilisé des goroutines et une carte synchronisée. J'ai aussi résolu l'exercice en utilisant des canaux. Le code source complet pour les deux solutions est disponible sur GitHub.

Regardons les parties pertinentes. Tout d'abord, voici une structure qui sera envoyée à un canal chaque fois qu'un goroutine analysera une page. Il contient la profondeur actuelle et toutes les URL trouvées sur la page..

liens de type struct urls [] string de profondeur int

le fetchURL () function accepte une URL, une profondeur et un canal de sortie. Il utilise l'outil de recherche (fourni par l'exercice) pour obtenir les URL de tous les liens de la page. Il envoie la liste des URL sous forme de message unique au canal du candidat sous forme de message. liens struct avec une profondeur décrémentée. La profondeur représente combien nous devrions explorer davantage. Lorsque la profondeur atteint 0, aucun traitement supplémentaire ne doit avoir lieu.

func fetchURL (chaîne d'URL, profondeur int, candidats liens) corps, URL, err: = fetcher.Fetch (url) fmt.Printf ("trouvé:% s% q \ n", url, corps) si err! = nil fmt.Println (err) candidats <- linksurls, depth - 1 

le ChannelCrawl () la fonction coordonne tout. Il garde une trace de toutes les URL déjà extraites dans une carte. Il n’est pas nécessaire de synchroniser l’accès, car aucune autre fonction ni aucun autre goroutine ne se touchent. Il définit également un canal candidat dans lequel toutes les goroutines écrivent leurs résultats..

Ensuite, il commence à invoquer parseUrl en tant que goroutines pour chaque nouvelle URL. La logique garde en mémoire combien de goroutines ont été lancées en gérant un compteur. Chaque fois qu'une valeur est lue sur le canal, le compteur est décrémenté (car le goroutine d'envoi est fermé après l'envoi) et chaque fois qu'un nouveau goroutine est lancé, le compteur est incrémenté. Si la profondeur atteint zéro, aucun nouveau goroutine ne sera lancé et la fonction principale continuera à lire le canal jusqu'à ce que toutes les goroutines soient terminées..

// ChannelCrawl explore les liens depuis une graine d'URL func ChannelCrawl (chaîne d'URL, profondeur int, récupérateur Fetcher) candidats: = make (liens chan, 0) fetched: = make (map [chaîne] bool) counter: = 1 // Fetch url initiale pour alimenter le canal des candidats, allez dans fetchURL (url, profondeur, candidats) pour 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 (candidat, profondeur, candidats)

Conclusion

Les canaux de Go offrent de nombreuses options pour une communication en toute sécurité entre les goroutines. La syntaxe est à la fois concise et illustrative. C'est une véritable aubaine pour l'expression d'algorithmes concurrents. Il y a beaucoup plus de chaînes que ce que j'ai présenté ici. Je vous encourage à plonger et à vous familiariser avec les divers modèles de concurrence qu’ils permettent..