3. Coût Mémoire & Bascule de Contexte
Comparatif matériel entre un thread noyau et une goroutine Go :
Thread OS : 1 à 2 Mo fixe réservés d'office.
Goroutine : ~2 Ko extensible dynamiquement.
Thread OS : appel système lourd (syscall).
Goroutine : allocation utilisateur en quelques nanosecondes.
Thread OS : 1000 à 2000 cycles CPU (kernel).
Goroutine : 10 à 20 cycles CPU (user-space).
4. L'Ordonnanceur Go & Work Stealing
L'ordonnanceur Go (modèle GMP) repose sur un triplet architectural et un équilibrage dynamique de charge :
Pile d'exécution extensible, pointeur d'instruction et code léger.
Unité logique.Thread noyau du système d'exploitation exécuté sur un cœur physique.
Thread matériel OS.Contexte d'exécution et file locale (défini par GOMAXPROCS).
File locale sans verrou.- Vol de Travail (Work Stealing) : Lorsqu'un P vide sa file, il vole la moitié des goroutines d'un autre P sans goulet d'étranglement central.
- Délestage I/O Bloquantes : Si une goroutine subit un syscall bloquant, M libère P pour qu'il continue d'exécuter d'autres tâches.
5. Verrous d'Exclusion Mutuelle
L'accès à une variable partagée en écriture impose de garantir l'exclusion mutuelle :
1type SafeCounter struct {
2 mu sync.Mutex
3 value int
4}
5
6func (c *SafeCounter) Increment() {
7 c.mu.Lock()
8 c.value++ // Section critique protégée
9 c.mu.Unlock()
10}
- Règle fondamentale : Une seule goroutine à la fois peut pénétrer dans la section critique.
- Question clé : Que se passe-t-il quand des dizaines de cœurs demandent le même verrou simultanément ?
6. Le Phénomène de Contention
Quand plusieurs cœurs se disputent le même verrou, les performances s'effondrent :
1. Les goroutines en attente sont suspendues (Parked) par l'ordonnanceur.
2. Le réveil des threads engendre des bascules de contexte et des invalidations de cacheline MESI en boucle.
3. Le coût de gestion du verrou devient supérieur au temps de calcul effectif.
7. L'Écroulement de la Loi d'Amdahl
Ajouter des cœurs CPU ne résout pas la contention : cela aggrave le blocage matériel :
- Loi d'Amdahl : L'accélération maximale d'un système est bornée par sa fraction strictement séquentielle.
- Loi d'Universal Scalability (USL) : Sous contention, l'effort de coordination inter-cœurs fait chuter le débit net.
- Conséquence directe : Au-delà d'un seuil critique, passer de 8 à 32 cœurs divise le débit au lieu de le multiplier !
8. Primitives Atomiques & Instructions LOCK
Les opérations atomiques manipulent la mémoire directement via le contrôleur matériel sans passer par le noyau :
1import "sync/atomic"
2
3type AtomicCounter struct {
4 value int64
5}
6
7func (c *AtomicCounter) Increment() {
8 atomic.AddInt64(&c.value, 1) // Compile en LOCK XADDQ au niveau silicium
9}
- Zéro appel système, zéro mise en veille : L'instruction processeur (
LOCK CMPXCHG/LOCK XADDQ) verrouille temporairement la ligne de cache pour exécuter la modification en 1 cycle.
9. Modèle Read-Copy-Update (RCU)
Pour les structures très souvent lues et rarement écrites (tables de routage, configurations) :
Les lecteurs accèdent directement aux données via un pointeur atomique en 0 ns sans contention.
Scalabilité infinie en lecture.L'écrivain duplique la structure, applique ses modifications sur la copie privée isolée.
Aucun blocage des lecteurs.L'écrivain intervertit le pointeur avec StorePointer. Les nouveaux lecteurs voient la nouvelle version.
Transition instantanée.10. Implémentation du Pattern RCU
Exemple canonique d'une table de routage avec lecture ultra-rapide et swap atomique sans blocage :
1type RoutingTable struct {
2 routes atomic.Pointer[map[string]Handler]
3}
4
5// Lecture ultra-rapide sans verrou (1 cycle)
6func (rt *RoutingTable) Get(path string) Handler {
7 m := rt.routes.Load()
8 if m == nil { return nil }
9 return (*m)[path]
10}
11
12// Mise à jour sur copie privée puis permutation atomique
13func (rt *RoutingTable) Set(path string, h Handler) {
14 oldMap := rt.routes.Load()
15 newMap := make(map[string]Handler)
16 if oldMap != nil {
17 for k, v := range *oldMap { newMap[k] = v }
18 }
19 newMap[path] = h
20 rt.routes.Store(&newMap) // Swap atomique instantané
21}
11. Synthèse Concurrence & Verrous
Les 3 règles d'or pour la concurrence backend :
Remplacer les verrous par sync/atomic pour les compteurs et drapeaux simples.
Instructions matérielles.Utiliser atomic.Pointer pour les données lues massivement et modifiées rarement.
Lectures sans verrou.Garder les sections protégées par Mutex aussi courtes que possible pour éviter la contention.
Débit préservé.TP Fil Rouge (Séance 5) : Parallélisation & Worker Pool Borné
Mission : Découper l'espace de recherche de la cible @kAl1, distribuer les lots sur un Worker Pool borné au nombre de cœurs CPU et propager l'annulation précoce instantanée.
1. Partitionnement Combinatoire
Découper l'espace de recherche (ex. partitionner par le premier caractère de @kAl1) et distribuer les blocs de candidats via un canal Go (channel).
2. Pool Borné sur runtime.NumCPU()
Construire un Worker Pool borné exactement sur le nombre de cœurs logiques de la machine pour éviter les pénalités d'ordonnancement liées au surplus de goroutines.
3. Arrêt Précoce context.WithCancel
Mettre en place l'arrêt précoce non bloquant : dès qu'un worker découvre le mot de passe, propager l'annulation instantanée à tous les workers via un context.WithCancel pour libérer immédiatement le CPU.
4. Scaling Multi-Cœurs
Mesurer l'accélération (Speedup) en fonction du nombre de cœurs alloués (1, 2, 4, N cœurs) et vérifier la linéarité du passage à l'échelle.