1. Exécutez chaque exemple : Ne vous contentez pas de lire le code. Tapez-le, exécutez-le et observez le comportement.⚠️ Comment s'y prendre pour cette série ?
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é.
Il s'agit de la partie 1 de notre série "Mastering Go Concurrency" où nous aborderons :
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.
nous serons présents tout au long du processus.
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) }
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 :
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!") }
attends quoi ? rien n'a été imprimé ? Pourquoi ?
Visualisons cela pour comprendre ce qui pourrait se passer.
à 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 :
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) }
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.
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!") }
Visualisons cela et comprenons le fonctionnement de sync.WaitGroup :
Mécanisme de compteur :
Flux de synchronisation :
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)
}
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.
LesChannels 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 :
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) }
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é.
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!") }
Visualisons ceci :
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.
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) }
le visualiser :
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 :
Boucle de chaîne :
Comportement de la boucle :
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
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) }
Visualisons ceci et exécutons ceci :
essai à sec :
Démarrage du programme (t=0ms)
Premier message (t=1ms)
Deuxième message (t=101ms)
Troisième message (t=201ms)
Fermeture de la chaîne (t=301 ms)
Achèvement (t=302-303ms)
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 :
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) }
sortie (avant de décommenter le ch<-"troisième")
Pourquoi n'a-t-il pas bloqué la goroutine principale ?
Un canal tamponné permet d'envoyer jusqu'à sa capacité sans bloquer l'expéditeur.
Le canal a une capacité de 2, ce qui signifie qu'il peut contenir deux valeurs dans son tampon avant de se bloquer.
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.
É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.
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. |
Utiliser les canaux bufféris si :
Utiliser les canaux sans tampon si :
Ces fondamentaux ouvrent la voie à des concepts plus avancés. Dans nos prochains articles, nous explorerons :
Message suivant :
Restez à l'écoute pendant que nous continuons à développer notre compréhension des puissantes fonctionnalités de concurrence de Go !
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!