
Parallélisme vs concurrence
Démystifiez le parallélisme et la concurrence en Go. Comprenez leurs différences fondamentales, leurs avantages, leurs cas d'usage et comment Go les implémente pour optimiser vos applications.
Distinguer parallélisme et concurrence : Deux concepts fondamentaux
Dans le domaine de l'informatique, les termes parallélisme et concurrence sont souvent employés, parfois de manière interchangeable, mais ils désignent des concepts distincts, bien que liés. Comprendre la nuance entre ces deux notions est essentiel pour appréhender pleinement la puissance de la concurrence en Go et pour concevoir des applications performantes et réactives.
La concurrence est une affaire de structure : elle consiste à organiser votre programme comme un ensemble de tâches indépendantes, qui peuvent progresser de manière apparemment simultanée. Le parallélisme, quant à lui, est une question d'exécution : il s'agit de faire réellement s'exécuter plusieurs tâches au même instant, en exploitant les ressources de calcul disponibles, notamment les processeurs multi-coeurs.
Ce chapitre vise à clarifier la distinction fondamentale entre parallélisme et concurrence, en explorant leurs définitions, leurs caractéristiques, leurs avantages et leurs cas d'utilisation respectifs. Nous verrons comment Go implémente la concurrence et comment il permet d'atteindre le parallélisme, et nous vous fournirons les clés pour choisir l'approche la plus adaptée à vos besoins et pour optimiser la performance de vos applications concurrentes Go.
Concurrence : Structurer pour gérer la simultanéité apparente
La concurrence est avant tout une technique de structuration du code. Elle permet de concevoir des programmes qui gèrent plusieurs tâches en apparence simultanément, même si, en réalité, ces tâches peuvent être exécutées de manière intercalée dans le temps, en particulier sur un système mono-coeur.
Imaginez un chef cuisinier (votre programme) qui doit préparer plusieurs plats (tâches) dans une cuisine (processeur). En concurrence, le chef ne travaille pas sur un seul plat à la fois jusqu'à la fin, mais alterne entre les différents plats, en passant d'une tâche à l'autre de manière rapide et efficace. Il peut préparer les légumes pour un plat pendant que la viande d'un autre plat marine, puis revenir aux légumes, et ainsi de suite. L'illusion est celle d'une préparation simultanée, même si le chef n'est qu'un seul.
En programmation concurrente, les tâches sont souvent indépendantes les unes des autres et peuvent progresser de manière asynchrone. La concurrence permet de gérer l'asynchronisme, d'améliorer la réactivité des applications (en évitant les blocages), et de simplifier la structure du code en décomposant des problèmes complexes en unités concurrentes plus petites et plus faciles à gérer.
Exemple de concurrence (sans parallélisme immédiat) :
package main
import (
"fmt"
"time"
)
func tache(nom string) {
fmt.Println(nom, "démarre")
time.Sleep(2 * time.Second) // Simuler une tâche longue
fmt.Println(nom, "termine")
}
func main() {
go tache("Tâche A")
go tache("Tâche B")
// Le programme principal continue sans attendre la fin des tâches
fmt.Println("Programme principal : les tâches sont lancées en concurrence")
time.Sleep(3 * time.Second) // Attendre un peu pour observer l'exécution concurrente
}
Dans cet exemple, même si votre machine n'a qu'un seul coeur de processeur, les tâches "Tâche A" et "Tâche B" s'exécutent de manière concurrente. Le programme principal lance les deux tâches en goroutines et continue son exécution. Le runtime Go se charge d'intercaler l'exécution des goroutines, donnant l'illusion d'une exécution simultanée, bien que, sur un mono-coeur, elles soient en réalité exécutées l'une après l'autre, mais de manière très rapide et intercalée.
Parallélisme : Exécuter réellement simultanément pour la performance
Le parallélisme, à la différence de la concurrence, vise à l'exécution simultanée de plusieurs tâches au même instant, en exploitant les capacités de plusieurs coeurs de processeur. Le parallélisme est une technique d'exécution, axée sur la performance et la rapidité, qui permet de réduire le temps d'exécution global d'un programme en divisant le travail entre plusieurs unités de traitement qui travaillent en parallèle.
Reprenons l'analogie du chef cuisinier. Pour atteindre le parallélisme, il faudrait non pas un seul chef, mais plusieurs chefs (processeurs multi-coeurs) travaillant simultanément dans la même cuisine. Chaque chef pourrait se concentrer sur un plat différent, et tous les plats seraient préparés en un temps considérablement réduit grâce à cette division du travail et à l'exécution parallèle.
En programmation parallèle, les tâches sont divisées et distribuées entre les différents coeurs de processeur, et s'exécutent réellement en même temps, améliorant significativement la performance pour les charges de travail CPU-bound (qui consomment beaucoup de temps processeur).
Exemple de parallélisme (avec plusieurs coeurs) :
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func tacheParallele(nom string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println(nom, "démarre sur le CPU", runtime.GOMAXPROCS(0)) // Afficher le CPU utilisé
time.Sleep(2 * time.Second) // Simuler une tâche longue
fmt.Println(nom, "termine")
}
func main() {
runtime.GOMAXPROCS(runtime.NumCPU()) // Activer le parallélisme en utilisant tous les coeurs disponibles
fmt.Println("GOMAXPROCS :", runtime.GOMAXPROCS(0))
var wg sync.WaitGroup
wg.Add(2)
go tacheParallele("Tâche 1", &wg)
go tacheParallele("Tâche 2", &wg)
fmt.Println("Programme principal : tâches lancées en parallèle")
wg.Wait() // Attendre la fin des tâches parallèles
fmt.Println("Programme principal : toutes les tâches parallèles sont terminées")
}
Dans cet exemple, en configurant runtime.GOMAXPROCS(runtime.NumCPU()), nous activons le parallélisme en demandant à Go d'utiliser tous les coeurs de processeur disponibles. Les tâches "Tâche 1" et "Tâche 2", lancées en goroutines, s'exécuteront potentiellement en parallèle sur des coeurs de processeur différents, réduisant le temps d'exécution total par rapport à une exécution purement concurrente sur un seul coeur.
Tableau comparatif : Concurrence vs. Parallélisme
Pour mieux visualiser les différences clés entre concurrence et parallélisme, voici un tableau comparatif récapitulatif :
| Caractéristique | Concurrence | Parallélisme |
|---|---|---|
| Objectif principal | Structurer le code, gérer l'asynchronisme, améliorer la réactivité | Améliorer la performance, réduire le temps d'exécution |
| Exécution des tâches | Apparemment simultanée, intercalée (time-slicing) | Réellement simultanée, en parallèle |
| Nombre de processeurs requis | Peut fonctionner sur un seul processeur (mono-coeur) | Nécessite plusieurs processeurs (multi-coeurs) pour un parallélisme réel |
| Amélioration de la performance | Améliore la réactivité, pas nécessairement la vitesse d'exécution (pour les tâches CPU-bound) | Améliore significativement la vitesse d'exécution pour les tâches parallélisables (CPU-bound) |
| Complexité de programmation | Complexité liée à la synchronisation et à la communication entre tâches | Complexité liée à la division du travail, à la coordination et à la gestion de la cohérence des données |
| Implémentation en Go | Goroutines (nativement concurrentes) | Goroutines + configuration GOMAXPROCS pour activer le parallélisme |
Ce tableau met en évidence que la concurrence et le parallélisme sont deux concepts complémentaires, mais distincts, avec des objectifs et des implications différents. Go excelle dans la gestion de la concurrence, et offre également des mécanismes pour exploiter le parallélisme lorsque la performance est un facteur critique.
Go et la dualité concurrence/parallélisme : Goroutines et GOMAXPROCS
Go offre une approche unique et puissante pour gérer la dualité concurrence/parallélisme grâce à ses goroutines et à la configuration GOMAXPROCS.
Goroutines : Concurrence native et légère
Les goroutines sont le pilier de la concurrence en Go. Elles permettent de structurer naturellement votre code de manière concurrente, en divisant votre programme en tâches indépendantes et légères qui peuvent progresser de manière apparemment simultanée. Go gère nativement la planification et l'exécution des goroutines de manière très efficace, même sur un seul thread d'OS, offrant une excellente réactivité et une grande simplicité pour la programmation concurrente.
GOMAXPROCS : Activer le parallélisme (si nécessaire)
La configuration GOMAXPROCS permet d'activer le parallélisme en Go, en contrôlant le nombre maximal de threads d'OS que le runtime Go peut utiliser pour exécuter les goroutines en parallèle. Par défaut, GOMAXPROCS est égal au nombre de coeurs logiques disponibles sur la machine, ce qui permet à Go d'exploiter le parallélisme multi-coeurs pour les tâches qui peuvent bénéficier d'une exécution simultanée.
Choisir entre concurrence et parallélisme en Go :
Le choix entre privilégier la concurrence ou le parallélisme en Go dépend des besoins spécifiques de votre application :
- Privilégier la concurrence (par défaut) : Dans la plupart des cas, la concurrence (via les goroutines) est suffisante et apporte déjà des avantages significatifs en termes de réactivité, de modularité et de gestion de l'asynchronisme. Go est excellent pour la programmation concurrente, et les goroutines offrent une approche simple et efficace pour structurer le code concurrent. Vous n'avez pas toujours besoin d'activer explicitement le parallélisme pour bénéficier des avantages de la concurrence en Go.
- Activer le parallélisme (avec
GOMAXPROCS) pour les tâches CPU-bound : Si votre application effectue des tâches CPU-bound (calculs intensifs, traitement de données volumineux) qui peuvent bénéficier d'une exécution réellement simultanée sur plusieurs coeurs, alors activer le parallélisme en configurantGOMAXPROCSà une valeur supérieure à 1 peut améliorer significativement la performance et la rapidité d'exécution. Mesurez et testez les performances de votre application avec et sans parallélisme pour déterminer si le parallélisme apporte un gain réel dans votre cas spécifique. - Combiner concurrence et parallélisme : Dans de nombreuses applications complexes, la meilleure approche consiste à combiner la concurrence et le parallélisme. Utilisez les goroutines pour structurer votre code de manière concurrente et gérer l'asynchronisme, et activez le parallélisme (via
GOMAXPROCS) pour exploiter les coeurs multi-processeurs et accélérer les parties CPU-bound de votre application.
Go offre la flexibilité de choisir entre la concurrence seule, le parallélisme, ou une combinaison des deux, permettant d'adapter l'approche de concurrence aux besoins spécifiques de chaque application et de chaque type de tâche.
Bonnes pratiques pour choisir et implémenter concurrence et parallélisme
Pour choisir et implémenter efficacement la concurrence et le parallélisme dans vos applications Go, voici quelques bonnes pratiques à suivre :
- Identifier les besoins de concurrence et de parallélisme de votre application : Analysez les besoins de votre application pour déterminer si la concurrence et/ou le parallélisme sont justifiés et bénéfiques. Pour les applications I/O-bound (serveurs web, applications réseau), la concurrence est souvent plus importante que le parallélisme. Pour les applications CPU-bound (calculs intensifs, traitement de données), le parallélisme peut apporter des gains de performance significatifs.
- Privilégier la concurrence pour la structure et la réactivité (par défaut) : Utilisez les goroutines pour structurer votre code de manière concurrente et gérer l'asynchronisme, même si vous ne visez pas nécessairement le parallélisme maximal. La concurrence apporte des avantages en termes de clarté, de modularité et de réactivité, même sur un seul coeur.
- Activer le parallélisme (GOMAXPROCS > 1) uniquement lorsque c'est nécessaire et bénéfique : N'activez le parallélisme (en configurant
GOMAXPROCS) que si vous avez des tâches CPU-bound qui peuvent réellement bénéficier d'une exécution parallèle sur plusieurs coeurs. Mesurez et testez les performances de votre application avec et sans parallélisme pour vérifier le gain réel. Dans de nombreux cas, la concurrence seule est suffisante et plus simple à gérer. - Utiliser les outils de profilage et de benchmarking de Go : Utilisez les outils de profilage (
pprof) et de benchmarking de Go pour analyser les performances de votre code concurrent et identifier les goulots d'étranglement ou les zones qui pourraient bénéficier du parallélisme. Le profilage et le benchmarking vous aident à prendre des décisions éclairées sur l'opportunité d'ajouter du parallélisme et sur la manière de l'optimiser. - Simplifier la synchronisation et la communication entre goroutines : Concevez votre code concurrent de manière à minimiser la complexité de la synchronisation et de la communication entre les goroutines. Utilisez les channels de manière idiomatique pour la communication et la synchronisation, et évitez autant que possible la mémoire partagée et les mécanismes de verrouillage complexes (mutex), sauf cas spécifiques.
- Documenter clairement les aspects concurrents de votre code : Documentez clairement les choix de conception en matière de concurrence et de parallélisme dans votre code, en expliquant pourquoi vous avez choisi une approche particulière, comment la concurrence et le parallélisme sont implémentés, et quelles sont les considérations de performance et de scalabilité. Une bonne documentation facilite la compréhension et la maintenance du code concurrent.
En appliquant ces bonnes pratiques, vous choisirez et implémenterez la concurrence et le parallélisme de manière pertinente et efficace dans vos applications Go, en optimisant à la fois la structure, la réactivité et la performance de votre code.