Retour au cours

backend / go

Select et patterns de concurrence

Leçon 131 exercice

Explication

Ce que vous allez apprendre

  • Utiliser select pour attendre sur plusieurs channels simultanément
  • Implémenter un timeout robuste avec select et time.After
  • Rendre un select non bloquant grâce à default
  • Construire un pattern fan-out / fan-in avec un pool de workers
  • Annuler proprement une opération en cours avec context

Dans quel contexte ?

Un service appelle une API externe qui peut parfois ne jamais répondre à cause d'un problème réseau côté fournisseur. Sans mécanisme de timeout, la requête resterait bloquée indéfiniment, gelant potentiellement toute une chaîne de traitement derrière elle. select combiné à time.After est le pattern standard pour éviter exactement ce scénario en production.

D'abord, select généralise l'idée d'attendre sur un channel

Alors qu'une simple réception <-ch bloque sur un seul channel, select surveille plusieurs channels à la fois et exécute le bloc correspondant au premier qui devient prêt. Si plusieurs channels sont prêts simultanément, Go en choisit un au hasard, pour éviter tout biais de priorité involontaire.

Cette capacité à "écouter sur plusieurs sources à la fois" est la brique de base de presque tous les patterns de concurrence avancés du langage.

Une fois ce mécanisme acquis, le pattern de timeout devient naturel

time.After(duree) retourne un channel qui reçoit une valeur unique après le délai indiqué. En le plaçant comme un cas parmi d'autres dans un select, on obtient un timeout élégant : soit le résultat attendu arrive à temps, soit le délai s'écoule en premier et on abandonne l'attente.

PatternComposantsEffet
Timeoutselect + time.AfterAbandonne l'attente après un délai
Non bloquantselect + defaultVérifie un channel sans jamais attendre
Fan-out / fan-inN goroutines + 1 channel partagéParalléliser un traitement avec un parallélisme borné
Annulationcontext.WithTimeout/WithCancelPropager un arrêt à travers plusieurs goroutines

Prérequis

Cette leçon combine directement goroutines et channels (deux leçons précédentes) : une bonne maîtrise des deux est indispensable pour suivre les exemples de worker pool.

Il reste un usage particulier de select à connaître : le mode non bloquant

Ajouter un cas default à un select change complètement son comportement : au lieu d'attendre qu'un channel soit prêt, il exécute immédiatement default si aucun channel ne l'est. C'est utile pour vérifier "y a-t-il un message en attente ?" sans jamais bloquer l'exécution du programme.

Piège fréquent

Un select sans default et sans aucun cas qui deviendra jamais prêt bloque indéfiniment ("deadlock"), et Go détecte ce cas au runtime avec un message explicite si c'est la seule goroutine active. Vérifie toujours qu'au moins un cas de ton select a une réelle chance de se réaliser.

Enfin, le pattern fan-out / fan-in structure le parallélisme

Plusieurs "workers" (goroutines identiques) consomment des tâches depuis un seul channel partagé, et envoient leurs résultats vers un channel commun. Ce pattern borne naturellement le parallélisme au nombre de workers lancés, évitant de saturer la machine avec un nombre incontrôlé de goroutines simultanées.

Bonne pratique

Pour annuler une opération à travers plusieurs goroutines de façon coordonnée, préfère toujours context.WithTimeout/WithCancel à un channel "done" fait maison : c'est l'idiome standard de toute la bibliothèque standard et de l'écosystème Go, et il sera approfondi dans une prochaine leçon.

Maintenant que tu sais orchestrer plusieurs channels, il te manque encore un outil pour protéger un état partagé quand les channels ne suffisent pas : place au package sync et à ses mutex.

Commandes & code

Select et patterns de concurrence

select attend sur plusieurs channels simultanément : la base des patterns de concurrence avancés.

go
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(50 * time.Millisecond)
		ch1 <- "resultat de ch1"
	}()
	go func() {
		time.Sleep(20 * time.Millisecond)
		ch2 <- "resultat de ch2"
	}()

	// select choisit le premier channel pret (ici ch2, plus rapide)
	for i := 0; i < 2; i++ {
		select {
		case msg1 := <-ch1:
			fmt.Println(msg1)
		case msg2 := <-ch2:
			fmt.Println(msg2)
		}
	}

	// Timeout avec select : pattern tres courant en production
	resultat := make(chan string)
	go func() {
		time.Sleep(200 * time.Millisecond)
		resultat <- "trop lent"
	}()
	select {
	case r := <-resultat:
		fmt.Println(r)
	case <-time.After(100 * time.Millisecond):
		fmt.Println("timeout depasse")
	}

	// select non-bloquant avec default
	messages := make(chan int, 1)
	select {
	case m := <-messages:
		fmt.Println("message:", m)
	default:
		fmt.Println("aucun message disponible")
	}

	// Fan-out / fan-in : plusieurs workers consomment une seule source
	jobs := make(chan int, 10)
	resultats := make(chan int, 10)
	for w := 1; w <= 3; w++ {
		go worker(w, jobs, resultats)
	}
	for j := 1; j <= 5; j++ {
		jobs <- j
	}
	close(jobs)
	for i := 0; i < 5; i++ {
		fmt.Println("resultat worker:", <-resultats)
	}

	// Annulation coordonnee avec un channel "done"
	ctx, annuler := context.WithTimeout(context.Background(), 50*time.Millisecond)
	defer annuler()
	longueTache(ctx)
}

func worker(id int, jobs <-chan int, resultats chan<- int) {
	for j := range jobs {
		resultats <- j * j
	}
}

func longueTache(ctx context.Context) {
	select {
	case <-time.After(500 * time.Millisecond):
		fmt.Println("tache terminee normalement")
	case <-ctx.Done():
		fmt.Println("tache annulee:", ctx.Err())
	}
}

Résumé

  • select bloque jusqu'à ce qu'un des cas soit prêt, choisi aléatoirement en cas d'égalité.
  • time.After + select implémente un timeout sans bibliothèque externe.
  • default dans un select le rend non-bloquant.
  • Fan-out (plusieurs workers) / fan-in (résultats agrégés) est le pattern de base du parallélisme en Go.

Exercices pratiques

1 disponible
1

Mission : un appel à un fournisseur externe qui gèle toute la chaîne

Objectif : Ajouter un timeout robuste sur un appel réseau externe et dimensionner un pool de workers, sans provoquer de deadlock ni de fuite de goroutines.

Contexte

Le service de paiement appelle une API d'un fournisseur externe connu pour parfois ne jamais répondre. Le code actuel fait directement resultat := <-appelerFournisseur(commande) sans aucune protection. Depuis un incident réseau chez ce fournisseur la semaine dernière, toute la chaîne de traitement des commandes s'est retrouvée figée pendant plus d'une heure.

Résoudre l’exercice →