MFormations
Modern Go Engineering

Chapitre 2

02 - Concurrence en Go

02 - Concurrence en Go

Cours 02 : Concurrence en Go

1. Goroutines

1.1 Concept et modèle M:N

Diagramme en cours de génération...

Le modèle M:N signifie que N goroutines sont multiplexées sur M threads OS.

// Lancement d'une goroutine
go func() {
    fmt.Println("Exécuté en parallèle")
}()

// Lancement avec paramètres
msg := "hello"
go func(s string) {
    fmt.Println(s)
}(msg)

1.2 Stack dynamique

func main() {
    // Une goroutine démarre avec ~2KB de stack
    // Le stack grandit/rétrécit automatiquement
    var stackSize int
    go func() {
        // Appels récursifs qui augmentent le stack
        var bigBuf [10000]byte
        _ = bigBuf
    }()
    
    // Vérifier la taille minimale de stack
    _ = stackSize
}

1.3 Goroutine lifecycle

func main() {
    done := make(chan bool)
    
    go func() {
        fmt.Println("Working...")
        time.Sleep(time.Second)
        done <- true
    }()
    
    <-done // Attend que la goroutine finisse
    fmt.Println("Done")
}

2. Channels

2.1 Création et types

// Channel non-buffered (synchrone)
ch := make(chan int)

// Channel buffered (asynchrone)
chBuf := make(chan string, 10)

// Channel en lecture seule
var readOnly <-chan int = ch

// Channel en écriture seule
var writeOnly chan<- int = ch

2.2 Opérations de base

func main() {
    ch := make(chan int)

    // Envoi (bloque jusqu'à réception)
    go func() {
        ch <- 42
    }()

    // Réception (bloque jusqu'à émission)
    val := <-ch
    fmt.Println(val)

    // Channel buffered
    bufCh := make(chan int, 3)
    bufCh <- 1
    bufCh <- 2
    bufCh <- 3
    // bufCh <- 4 // BLOCK: buffer plein

    fmt.Println(<-bufCh) // 1
    fmt.Println(<-bufCh) // 2
    fmt.Println(<-bufCh) // 3
}

2.3 Range et Close

func producer(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch) // Important : le sender ferme le channel
}

func consumer(ch <-chan int) {
    for val := range ch { // Lit jusqu'à fermeture
        fmt.Printf("Reçu: %d\n", val)
    }
}

func main() {
    ch := make(chan int, 5)
    go producer(ch)
    consumer(ch)
}

2.4 Select

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

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "un"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "deux"
    }()

    select {
    case msg1 := <-ch1:
        fmt.Println("Reçu de ch1:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Reçu de ch2:", msg2)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Timeout!")
    default:
        fmt.Println("Aucun channel prêt")
    }
}

2.5 Pattern de sélection multiple

func main() {
    ch := make(chan int, 1)
    
    for i := 0; i < 10; i++ {
        select {
        case ch <- i:
            // Envoyé
        default:
            // Channel plein, on passe
        }
    }
}

3. Patterns de concurrence

3.1 Generator Pattern

func fibonacci(n int) <-chan int {
    ch := make(chan int)
    go func() {
        a, b := 0, 1
        for i := 0; i < n; i++ {
            ch <- a
            a, b = b, a+b
        }
        close(ch)
    }()
    return ch
}

func main() {
    for fib := range fibonacci(10) {
        fmt.Println(fib)
    }
}

3.2 Fan-In (Multiplexage)

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup
    
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for val := range c {
                out <- val
            }
        }(ch)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

func main() {
    c1 := generator(1, 3, 5, 7, 9)
    c2 := generator(2, 4, 6, 8, 10)
    
    for val := range fanIn(c1, c2) {
        fmt.Println(val)
    }
}

3.3 Fan-Out (Distribution)

func fanOut(in <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    
    for i := 0; i < workers; i++ {
        out := make(chan int)
        go func(id int) {
            for val := range in {
                fmt.Printf("Worker %d traite %d\n", id, val)
                out <- val * 2
            }
            close(out)
        }(i)
        channels[i] = out
    }
    
    return channels
}

3.4 Pipeline Pattern

func pipeline(stages ...func(<-chan int) <-chan int) <-chan int {
    var ch <-chan int
    
    // Stage 0 : source
    ch = stages[0](nil)
    
    // Stages intermédiaires
    for _, stage := range stages[1:] {
        ch = stage(ch)
    }
    
    return ch
}

// Étapes du pipeline
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func filterOdd(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            if n%2 == 0 {
                out <- n
            }
        }
        close(out)
    }()
    return out
}

func main() {
    // Pipeline: generate → square → filterOdd
    for result := range pipeline(generate, square, filterOdd) {
        fmt.Println(result)
    }
}

3.5 Worker Pool

type Job struct {
    ID   int
    Data string
}

type Result struct {
    JobID int
    Output string
}

func worker(id int, jobs <-chan Job, results chan<- Result) {
    for job := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, job.ID)
        results <- Result{
            JobID:  job.ID,
            Output: fmt.Sprintf("Processed: %s", job.Data),
        }
    }
}

func main() {
    const numJobs = 10
    const numWorkers = 3

    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)

    // Démarrer les workers
    for w := 1; w <= numWorkers; w++ {
        go worker(w, jobs, results)
    }

    // Envoyer les jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- Job{ID: j, Data: fmt.Sprintf("data-%d", j)}
    }
    close(jobs)

    // Collecter les résultats
    for r := 1; r <= numJobs; r++ {
        result := <-results
        fmt.Printf("Result: %+v\n", result)
    }
}

4. Synchronisation

4.1 sync.Mutex

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    var wg sync.WaitGroup
    counter := Counter{}

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println(counter.Value()) // 1000
}

4.2 sync.RWMutex

type Cache struct {
    mu    sync.RWMutex
    data  map[string]any
}

func (c *Cache) Get(key string) (any, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    val, ok := c.data[key]
    return val, ok
}

func (c *Cache) Set(key string, value any) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

4.3 sync.WaitGroup

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Worker %d démarré\n", id)
            time.Sleep(time.Duration(id) * 100 * time.Millisecond)
            fmt.Printf("Worker %d terminé\n", id)
        }(i)
    }

    wg.Wait()
    fmt.Println("Tous les workers terminés")
}

4.4 sync.Once

var (
    config   *Config
    configMu sync.Once
)

func GetConfig() *Config {
    configMu.Do(func() {
        fmt.Println("Initialisation de la configuration...")
        config = loadConfig()
    })
    return config
}

func main() {
    // Appels concurrents, une seule initialisation
    go GetConfig()
    go GetConfig()
    GetConfig()
}

4.5 sync.Cond

type Queue struct {
    items []int
    cond  *sync.Cond
}

func NewQueue() *Queue {
    return &Queue{
        items: make([]int, 0),
        cond:  sync.NewCond(&sync.Mutex{}),
    }
}

func (q *Queue) Enqueue(item int) {
    q.cond.L.Lock()
    defer q.cond.L.Unlock()
    q.items = append(q.items, item)
    q.cond.Signal() // Réveille un goroutine en attente
}

func (q *Queue) Dequeue() int {
    q.cond.L.Lock()
    defer q.cond.L.Unlock()
    for len(q.items) == 0 {
        q.cond.Wait() // Attend qu'un item soit disponible
    }
    item := q.items[0]
    q.items = q.items[1:]
    return item
}

4.6 sync.Pool

type BigBuffer struct {
    buf [1024]byte
}

var bufferPool = sync.Pool{
    New: func() any {
        return &BigBuffer{}
    },
}

func processRequest() {
    buf := bufferPool.Get().(*BigBuffer)
    defer bufferPool.Put(buf)
    
    // Utiliser buf
    // Pas besoin de réinitialiser, le pool gère
}

5. Context

5.1 Création de context

func main() {
    // Context vide (racine)
    ctx := context.Background()
    
    // Context TODO (à remplacer)
    ctx = context.TODO()

    // Avec annulation
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Avec timeout
    ctx, cancel = context.WithTimeout(ctx, 5*time.Second)
    defer cancel()

    // Avec deadline
    ctx, cancel = context.WithDeadline(ctx, time.Now().Add(5*time.Second))
    defer cancel()

    // Avec valeurs
    ctx = context.WithValue(ctx, "key", "value")
}

5.2 Propagation

func handleRequest(ctx context.Context) error {
    // Propagation aux sous-appels
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    result, err := fetchData(ctx)
    if err != nil {
        return fmt.Errorf("fetch data: %w", err)
    }

    return processResult(ctx, result)
}

func fetchData(ctx context.Context) (string, error) {
    done := make(chan string, 1)

    go func() {
        time.Sleep(3 * time.Second) // Simulation d'appel long
        done <- "data"
    }()

    select {
    case result := <-done:
        return result, nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

5.3 Context avec valeurs

type contextKey string

const (
    UserIDKey    contextKey = "user_id"
    TraceIDKey   contextKey = "trace_id"
    RequestIDKey contextKey = "request_id"
)

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        ctx = context.WithValue(ctx, TraceIDKey, uuid.New().String())
        ctx = context.WithValue(ctx, UserIDKey, r.Header.Get("X-User-ID"))
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    traceID := ctx.Value(TraceIDKey).(string)
    userID := ctx.Value(UserIDKey).(string)
    
    fmt.Printf("Trace: %s, User: %s\n", traceID, userID)
}

6. GOMAXPROCS et Race Detector

6.1 GOMAXPROCS

func main() {
    // Nombre de CPUs logiques
    fmt.Println("CPUs:", runtime.NumCPU())
    
    // Définir le nombre de threads OS
    runtime.GOMAXPROCS(runtime.NumCPU())
    
    // Vérifier
    fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))
}

6.2 Race Detector

# Activer le race detector
go run -race main.go
go build -race -o program main.go
go test -race ./...
// race_example.go - À EXÉCUTER AVEC -race
func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // DATA RACE !
        }()
    }

    wg.Wait()
    fmt.Println(counter)
}

7. Modèle GMP

Diagramme en cours de génération...

Résumé

  • Goroutines : légères (~2KB), multiplexées sur threads OS (M:N)
  • Channels : communication synchronisée entre goroutines
  • Patterns : fan-in, fan-out, pipeline, worker pool
  • Synchronisation : Mutex, RWMutex, WaitGroup, Once, Cond, Pool
  • Context : propagation d'annulation, timeout, valeurs
  • Race detector : outil essentiel pour la concurrence