Maison > développement back-end > Golang > Comprendre les Goroutines et les canaux dans Golang avec des visuels intuitifs

Comprendre les Goroutines et les canaux dans Golang avec des visuels intuitifs

Mary-Kate Olsen
Libérer: 2024-12-30 16:18:09
original
782 Les gens l'ont consulté

⚠️ Comment s'y prendre pour cette série ?

1. Exécutez chaque exemple : Ne vous contentez pas de lire le code. Tapez-le, exécutez-le et observez le comportement.
2. Expérimentez et cassez des choses : Supprimez les mises en veille et voyez ce qui se passe, modifiez la taille des tampons de canal, modifiez le nombre de goroutines.
Casser des choses vous apprend comment elles fonctionnent
3. Raison concernant le comportement : Avant d'exécuter du code modifié, essayez de prédire le résultat. Lorsque vous constatez un comportement inattendu, faites une pause et réfléchissez à pourquoi. Contestez les explications.
4. Construire des modèles mentaux : Chaque visualisation représente un concept. Essayez de dessiner vos propres diagrammes pour le code modifié.

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Il s'agit de la partie 1 de notre série "Mastering Go Concurrency" où nous aborderons :

  • Comment fonctionnent les goroutines et leur cycle de vie
  • Communication de canal entre les goroutines
  • Canaux tamponnés et leurs cas d'utilisation
  • Exemples pratiques et visualisations

Nous commencerons par les bases et avancerons progressivement en développant l'intuition sur la façon de les utiliser efficacement.

Ça va être un peu long, plutôt très long alors préparez-vous.

Understanding Goroutines and Channels in Golang with Intuitive Visuals

nous serons présents tout au long du processus.

Fondations des Goroutines

Commençons par un programme simple qui télécharge plusieurs fichiers.

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Le programme prend 6 secondes au total car chaque téléchargement de 2 secondes doit être terminé avant le démarrage du suivant. Visualisons ceci :

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Nous pouvons réduire ce temps, modifions notre programme pour utiliser des routines go :

avis : go mot-clé avant l'appel de fonction

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

attends quoi ? rien n'a été imprimé ? Pourquoi ?

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Visualisons cela pour comprendre ce qui pourrait se passer.

Understanding Goroutines and Channels in Golang with Intuitive Visuals

à partir de la visualisation ci-dessus, nous comprenons que la fonction principale existe avant que les goroutines ne soient terminées. Une observation est que le cycle de vie de tout goroutine dépend de la fonction principale.

Remarque : la fonction principale en elle-même est une goroutine ;)

Pour résoudre ce problème, nous avons besoin d'un moyen pour que la goroutine principale attende que les autres goroutines se terminent. Il existe plusieurs façons de procéder :

  1. attendez quelques secondes (de manière piratée)
  2. Utiliser WaitGroup (de la bonne manière, ensuite)
  3. Utiliser canaux (nous aborderons cela ci-dessous)

Attendons quelques secondes que les routines de départ se terminent.

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Le problème est que nous ne savons peut-être pas combien de temps une goroutine peut prendre. Dans notre cas, nous avons une durée constante pour chacun, mais dans des scénarios réels, nous sommes conscients que la durée de téléchargement varie.

Vient le sync.WaitGroup

Un sync.WaitGroup dans Go est un mécanisme de contrôle de concurrence utilisé pour attendre la fin de l'exécution d'une collection de goroutines.

Voyons cela en action et visualisons :

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Visualisons cela et comprenons le fonctionnement de sync.WaitGroup :

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Mécanisme de compteur :

  • WaitGroup maintient un compteur interne
  • wg.Add(n) augmente le compteur de n
  • wg.Done() décrémente le compteur de 1
  • wg.Wait() bloque jusqu'à ce que le compteur atteigne 0

Flux de synchronisation :

  • Les goroutines principales appellent Add(3) avant de lancer les goroutines
  • Chaque goroutine appelle Done() une fois l'opération terminée
  • La goroutine principale est bloquée à Wait() jusqu'à ce que le compteur atteigne 0
  • Lorsque le compteur atteint 0, le programme continue et se termine proprement

Les pièges courants à éviter
package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now() // Record start time

    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    // Wait for goroutines to finish
    time.Sleep(3 * time.Second)

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion

Canaux

Nous avons donc une bonne compréhension du fonctionnement des goroutines. Non, comment deux routines Go communiquent-elles ? C'est là qu'intervient la chaîne.

Les

Channels dans Go sont une puissante primitive de concurrence utilisée pour la communication entre les goroutines. Ils permettent aux goroutines de partager des données en toute sécurité.

Considérez les canaux comme des tuyaux : une goroutine peut envoyer des données dans un canal et une autre peut les recevoir.

voici quelques propriétés :

  1. Les chaînes sont bloquées par nature.
  2. Une opération envoyer au canal ch <- valeur blocs jusqu'à ce qu'une autre goroutine reçoive du canal.
  3. Une réception du canal opération <-ch blocs jusqu'à ce qu'une autre goroutine envoie au canal.
package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Understanding Goroutines and Channels in Golang with Intuitive Visuals

pourquoi ch <- "hello" provoquera-t-il une impasse ? Puisque les chaînes sont par nature bloquantes et que nous passons ici "bonjour", cela bloquera la goroutine principale jusqu'à ce qu'il y ait un récepteur et comme il n'y a pas de récepteur donc il sera bloqué.

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Résolvons cela en ajoutant une goroutine

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    // Launch downloads concurrently
    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    fmt.Println("All downloads completed!")
}
Copier après la connexion
Copier après la connexion
Copier après la connexion

Visualisons ceci :

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Ce message est envoyé depuis une goroutine différente afin que la goroutine principale ne soit pas bloquée lors de l'envoi au canal afin qu'il se déplace vers msg := <-ch où il bloque la goroutine principale jusqu'à ce qu'elle reçoive le message.

Correction du problème principal n'attendant pas les autres en utilisant le canal

Utilisons maintenant le canal pour résoudre le problème du téléchargement de fichiers (le principal n'attend pas que les autres terminent).

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now() // Record start time

    go downloadFile("file1.txt")
    go downloadFile("file2.txt")
    go downloadFile("file3.txt")

    // Wait for goroutines to finish
    time.Sleep(3 * time.Second)

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion

Understanding Goroutines and Channels in Golang with Intuitive Visuals

le visualiser :

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Faisons un essai pour mieux comprendre :

Démarrage du programme :

La goroutine principale crée un canal terminé
Lance trois goroutines de téléchargement
Chaque goroutine obtient une référence au même canal

Télécharger l'exécution :

  1. Les trois téléchargements s'exécutent simultanément
  2. Chacun prend 2 secondes
  3. Ils pourraient finir dans n'importe quel ordre

Boucle de chaîne :

  1. La goroutine principale entre dans la boucle : for i := 0 ; je &Lt ; 3 ; je
  2. Chaque <-done bloque jusqu'à ce qu'une valeur soit reçue
  3. La boucle garantit que nous attendons les trois signaux d'achèvement

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Comportement de la boucle :

  1. Itération 1 : Bloque jusqu'à la fin du premier téléchargement
  2. Itération 2 : Bloque jusqu'à la fin du deuxième téléchargement
  3. Itération 3 : Bloque jusqu'à la fin du téléchargement final

L'ordre d'achèvement n'a pas d'importance !

Observations :
⭐ Chaque envoi (terminé <- vrai) a exactement une réception (<-done)
⭐ La goroutine principale coordonne tout à travers la boucle

Comment deux goroutines peuvent-elles communiquer ?

Nous avons déjà vu comment deux goroutines peuvent communiquer. Quand? Pendant tout ce temps. N'oublions pas que la fonction principale est aussi une goroutine.

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Visualisons ceci et exécutons ceci :

Understanding Goroutines and Channels in Golang with Intuitive Visuals

essai à sec :

Démarrage du programme (t=0ms)

  • La goroutine principale initialise trois canaux :
    • ch : pour les messages.
    • senderDone : pour signaler l'achèvement de l'expéditeur.
    • ReceiverDone : pour signaler l'achèvement du récepteur.
  • La goroutine principale lance deux goroutines :
    • expéditeur.
    • récepteur.
  • Les blocs goroutine principaux, en attente d'un signal de <-senderDone.

Premier message (t=1ms)

  1. L'expéditeur envoie le "message 1" au canal ch.
  2. Le récepteur se réveille et traite le message :
    • Impressions : "Reçu : message 1".
  3. L'expéditeur dort pendant 100 ms.

Deuxième message (t=101ms)

  1. L'expéditeur se réveille et envoie le "message 2" au canal ch.
  2. Le destinataire traite le message :
    • Impressions : "Reçu : message 2".
  3. L'expéditeur dort pendant encore 100 ms.

Troisième message (t=201ms)

  1. L'expéditeur se réveille et envoie le "message 3" au canal ch.
  2. Le destinataire traite le message :
    • Impressions : "Reçu : message 3".
  3. L'expéditeur dort pour la dernière fois.

Fermeture de la chaîne (t=301 ms)

  1. L'expéditeur finit de dormir et ferme le canal ch.
  2. L'expéditeur envoie un vrai signal au canal senderDone pour indiquer l'achèvement.
  3. Le récepteur détecte que le canal ch a été fermé.
  4. Le récepteur quitte sa boucle for-range.

Achèvement (t=302-303ms)

  1. La goroutine principale reçoit le signal de senderDone et arrête d'attendre.
  2. La goroutine principale commence à attendre un signal du récepteurDone.
  3. Le récepteur envoie un signal de fin au canal ReceiverDone.
  4. La goroutine principale reçoit le signal et imprime :
    • "Toutes les opérations terminées !".
  5. Le programme se termine.

Canaux tamponnés

Pourquoi avons-nous besoin de canaux tamponnés ?
Les canaux sans tampon bloquent à la fois l'émetteur et le récepteur jusqu'à ce que l'autre côté soit prêt. Lorsqu'une communication haute fréquence est requise, les canaux sans tampon peuvent devenir un goulot d'étranglement car les deux goroutines doivent faire une pause pour échanger des données.

Propriétés des canaux tamponnés :

  1. FIFO (First In, First Out, similaire à la file d'attente)
  2. Taille fixe, définie à la création
  3. Bloque l'expéditeur lorsque le tampon est plein
  4. Bloque le récepteur lorsque le tampon est vide

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Nous le voyons en action :

package main

import (
    "fmt"
    "time"
)

func downloadFile(filename string) {
    fmt.Printf("Starting download: %s\n", filename)
    // Simulate file download with sleep
    time.Sleep(2 * time.Second)
    fmt.Printf("Finished download: %s\n", filename)
}

func main() {
    fmt.Println("Starting downloads...")

    startTime := time.Now()

    downloadFile("file1.txt")
    downloadFile("file2.txt")
    downloadFile("file3.txt")

    elapsedTime := time.Since(startTime)

    fmt.Printf("All downloads completed! Time elapsed: %s\n", elapsedTime)
}
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

sortie (avant de décommenter le ch<-"troisième")

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Pourquoi n'a-t-il pas bloqué la goroutine principale ?

  1. Un canal tamponné permet d'envoyer jusqu'à sa capacité sans bloquer l'expéditeur.

  2. Le canal a une capacité de 2, ce qui signifie qu'il peut contenir deux valeurs dans son tampon avant de se bloquer.

  3. Le tampon est déjà plein avec "premier" et "seconde". Puisqu'il n'y a pas de récepteur simultané pour consommer ces valeurs, l'opération d'envoi se bloque indéfiniment.

  4. Étant donné que la goroutine principale est également responsable de l'envoi et qu'il n'y a pas d'autres goroutines actives pour recevoir les valeurs du canal, le programme entre dans une impasse lorsqu'il tente d'envoyer le troisième message.

Décommenter le troisième message entraîne un blocage car la capacité est maintenant pleine et le 3ème message se bloquera jusqu'à ce que la mémoire tampon soit libérée.

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Quand utiliser les canaux avec tampon et les canaux sans tampon

Aspect Buffered Channels Unbuffered Channels
Purpose For decoupling sender and receiver timing. For immediate synchronization between sender and receiver.
When to Use - When the sender can proceed without waiting for receiver. - When sender and receiver must synchronize directly.
- When buffering improves performance or throughput. - When you want to enforce message-handling immediately.
Blocking Behavior Blocks only when buffer is full. Sender blocks until receiver is ready, and vice versa.
Performance Can improve performance by reducing synchronization. May introduce latency due to synchronization.
Example Use Cases - Logging with rate-limited processing. - Simple signaling between goroutines.
- Batch processing where messages are queued temporarily. - Hand-off of data without delay or buffering.
Complexity Requires careful buffer size tuning to avoid overflows. Simpler to use; no tuning needed.
Overhead Higher memory usage due to the buffer. Lower memory usage; no buffer involved.
Concurrency Pattern Asynchronous communication between sender and receiver. Synchronous communication; tight coupling.
Error-Prone Scenarios Deadlocks if buffer size is mismanaged. Deadlocks if no goroutine is ready to receive or send.

Points clés à retenir

Utiliser les canaux bufféris si :

  1. Vous devez découpler le timing de l'expéditeur et du destinataire.
  2. Les performances peuvent bénéficier du traitement par lots ou de la mise en file d'attente des messages.
  3. L'application peut tolérer des retards dans le traitement des messages lorsque le tampon est plein.

Utiliser les canaux sans tampon si :

  1. La synchronisation est essentielle entre les goroutines.
  2. Vous souhaitez de la simplicité et un transfert immédiat des données.
  3. L'interaction entre l'expéditeur et le destinataire doit se produire instantanément.

Ces fondamentaux ouvrent la voie à des concepts plus avancés. Dans nos prochains articles, nous explorerons :

Message suivant :

  1. Modèles de concurrence
  2. Synchronisation mutex et mémoire

Restez à l'écoute pendant que nous continuons à développer notre compréhension des puissantes fonctionnalités de concurrence de Go !

Understanding Goroutines and Channels in Golang with Intuitive Visuals

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal