Concurrence et parallélisme
Introduction à la concurrence en Go
Go est conçu pour gérer efficacement la concurrence, c’est-à-dire l’exécution simultanée de plusieurs tâches. Grâce aux goroutines et aux channels, Go offre un modèle de concurrence simple et performant, adapté aux applications modernes nécessitant un traitement parallèle.
1. Pourquoi la concurrence est-elle importante ?
La concurrence permet d’exécuter plusieurs tâches en parallèle, améliorant ainsi les performances des applications. Elle est essentielle notamment pour :
-
le traitement simultané de requêtes HTTP dans un serveur web ;
-
l’exécution de calculs intensifs en parallèle ;
-
la gestion de tâches d’arrière-plan sans bloquer l’application principale.
2. Les goroutines
Les goroutines permettent d’exécuter plusieurs tâches en parallèle de manière simple et efficace, sans gérer directement des threads complexes. Elles facilitent la programmation concurrente en Go, et ce sujet sera abordé plus en détail dans la section Les goroutines : exécution parallèle simplifiée.
3. Synchronisation avec les channels
Lorsque plusieurs goroutines s’exécutent simultanément, il est essentiel d’assurer une communication efficace entre elles pour éviter les conditions...
Les goroutines : exécution parallèle simplifiée
Les goroutines sont l’un des éléments clés de la concurrence en Go. Elles permettent d’exécuter des fonctions de manière asynchrone sans avoir recours aux threads classiques, offrant ainsi une gestion légère et efficace du parallélisme.
1. Qu’est-ce qu’une goroutine ?
Une goroutine est une fonction qui s’exécute indépendamment du programme principal. Contrairement aux threads, elles consomment très peu de mémoire et sont gérées directement par le runtime de Go.
2. Lancer une goroutine
L’exécution concurrente d’une fonction est l’un des aspects fondamentaux de la programmation avec Go. Une goroutine permet d’exécuter une fonction indépendamment du thread principal, sans complexité supplémentaire. Cette légèreté en fait un atout pour le développement d’applications nécessitant des opérations parallèles sans bloquer l’exécution principale.
a. Création et exécution d’une goroutine
Lancer une goroutine consiste à préfixer l’appel d’une fonction par le mot-clé go. Une fois la goroutine démarrée, son exécution se poursuit indépendamment du programme principal.
package main
import (
"fmt"
"time"
)
func printMessage() {
fmt.Println("Message affiché depuis une goroutine")
}
func main() {
go printMessage() // Exécution en arrière-plan
fmt.Println("Le programme principal continue son exécution")
time.Sleep(time.Second) // Pause pour observer l'affichage
}
b. Explication du comportement
Dans cet exemple, nous illustrons le fonctionnement d’une goroutine en Go et son impact sur l’exécution du programme principal :
-
go printMessage() démarre la fonction printMessage dans une nouvelle goroutine.
-
Le programme principal continue immédiatement son exécution sans attendre que la goroutine...
Communication entre Goroutines avec les canaux (channels)
L’exécution concurrente de plusieurs goroutines nécessite souvent un moyen de communication sécurisé pour échanger des données entre elles. Les channels (ou canaux) permettent cette communication en offrant une alternative plus simple et plus sûre que le partage de mémoire avec des verrous.
Un channel agit comme une file d’attente permettant d’envoyer et de recevoir des valeurs entre goroutines de manière synchrone ou asynchrone.
1. Création et utilisation de base d’un channel
Un channel est créé avec la fonction make() en spécifiant le type de données qu’il transporte. Une goroutine peut envoyer des données dans un channel, tandis qu’une autre peut les recevoir.
package main
import (
"fmt"
)
func main() {
// Création d'un channel de type string
messageChannel := make(chan string)
// Démarrage d'une goroutine pour envoyer un message
go func() {
messageChannel <- "Bonjour depuis la goroutine !"
}()
// Réception du message envoyé par la goroutine
receivedMessage := <-messageChannel
fmt.Println("Message reçu :", receivedMessage)
}
Explication
-
make(chan string) : crée un channel capable d’envoyer et de recevoir des chaînes de caractères.
-
messageChannel <- "Bonjour depuis la goroutine !" : envoie...
Synchronisation des données partagées
En programmation concurrente, la gestion des accès aux données partagées est essentielle pour éviter les conflits et garantir l’intégrité des informations. Go propose plusieurs mécanismes pour synchroniser l’accès aux ressources communes, notamment les mutex, les atomic operations et les channels.
1. Problèmes liés aux accès concurrents
Lorsque plusieurs goroutines accèdent simultanément à une même variable sans synchronisation, des comportements imprévisibles peuvent se produire, comme :
-
des valeurs incorrectes dues à des mises à jour concurrentes ;
-
des conditions de course (race conditions), rendant le programme non déterministe ;
-
des crashs en raison d’un accès incohérent à la mémoire.
2. Utilisation des mutex (sync.Mutex)
Lorsque plusieurs goroutines accèdent à une même variable partagée sans synchronisation, des comportements imprévisibles peuvent survenir. Une condition de course peut se produire si deux goroutines tentent de modifier une même valeur en même temps, entraînant des résultats incorrects ou incohérents.
Le package sync fournit un mécanisme de verrouillage appelé sync.Mutex, qui garantit qu’une seule goroutine accède à une ressource partagée à un instant donné.
a. Principe de fonctionnement d’un mutex
Un mutex (abréviation de "mutual exclusion") permet d’exclure temporairement toutes les autres goroutines de l’accès à une ressource jusqu’à ce que l’accès en cours soit terminé.
L’utilisation d’un sync.Mutex repose sur deux opérations principales :
-
Lock() : verrouille la ressource pour empêcher d’autres goroutines d’y accéder.
-
Unlock() : déverrouille la ressource une fois l’opération terminée, permettant aux autres goroutines d’y accéder.
b. Exemple d’utilisation d’un mutex
package main
import (
"fmt"
"sync"
"time"
)
type Counter struct { ...Cas pratique : construire un serveur concurrent
Les applications modernes nécessitent souvent un traitement efficace des requêtes en parallèle pour garantir une réponse rapide aux utilisateurs. Un serveur concurrent est capable de gérer plusieurs connexions simultanées sans bloquer l’exécution principale. Go fournit un support natif pour la concurrence grâce aux goroutines et aux channels, ce qui permet de concevoir des serveurs performants avec un minimum d’effort.
1. Mise en place d’un serveur HTTP simple
Le package net/http permet de créer un serveur capable de répondre aux requêtes entrantes. Chaque requête est gérée dans une goroutine distincte, ce qui permet un traitement concurrent sans effort supplémentaire.
package main
import (
"fmt"
"net/http"
"time"
)
func requestHandler(w http.ResponseWriter, r *http.Request) {
fmt.Printf("Requête reçue : %s\n", r.URL.Path)
time.Sleep(time.Second) // Simulation d'un traitement long
fmt.Fprintf(w, "Réponse envoyée à %s\n",
time.Now().Format(time.RFC3339))
}
func main() {
http.HandleFunc("/", requestHandler)
fmt.Println("Serveur démarré sur http://localhost:8080")
http.ListenAndServe(":8080", nil)
}
Explication
-
http.HandleFunc("/", requestHandler) : associe la route / à la fonction requestHandler.
-
time.Sleep(time.Second) : simule un traitement nécessitant du temps.
-
http.ListenAndServe(":8080", nil) : lance le serveur sur le port 8080.
Chaque requête est automatiquement traitée dans une nouvelle goroutine...