EMZETT.
Login

Kurz: Go macht Nebenläufigkeit zum Sprachmerkmal. Das Motto: “Teile Speicher nicht durch Kommunikation, sondern kommuniziere, um Speicher zu teilen.”

Teil des Kurses Go

Kapitel 8 von 11 im Kurs Go (Abschnitt „Nebenläufigkeit“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.

Go macht Nebenläufigkeit zum Sprachmerkmal. Das Motto: “Teile Speicher nicht durch Kommunikation, sondern kommuniziere, um Speicher zu teilen.”

Goroutinen

Eine Goroutine ist ein sehr leichtgewichtiger Ablauf (wenige KB). Du startest sie mit go:

package main
 
import (
    "fmt"
    "sync"
    "time"
)
 
func arbeite(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    time.Sleep(time.Duration(id) * 10 * time.Millisecond)
    fmt.Println("Goroutine", id, "fertig")
}
 
func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go arbeite(i, &wg)
    }
    wg.Wait() // wartet, bis alle fertig sind
    fmt.Println("alle fertig")
}

Ausgabe:

Goroutine 1 fertig
Goroutine 2 fertig
Goroutine 3 fertig
alle fertig

Endet main, endet das ganze Programm, auch wenn noch Goroutinen laufen. Deshalb wartet man mit sync.WaitGroup oder Channels.

Channels

Ein Channel transportiert Werte sicher zwischen Goroutinen:

package main
 
import "fmt"
 
func main() {
    ch := make(chan int) // ungepuffert: Senden wartet auf Empfänger
    go func() {
        for i := 1; i <= 3; i++ {
            ch <- i * i // senden
        }
        close(ch) // Ende signalisieren
    }()
    for wert := range ch { // empfangen, bis geschlossen
        fmt.Println("empfangen:", wert)
    }
 
    puffer := make(chan string, 2) // gepuffert: bis zu 2 Werte ohne Empfänger
    puffer <- "a"
    puffer <- "b"
    fmt.Println(len(puffer), cap(puffer), <-puffer, <-puffer)
 
    v, ok := <-ch // aus geschlossenem Channel: Nullwert, ok = false
    fmt.Println(v, ok)
}

Ausgabe:

empfangen: 1
empfangen: 4
empfangen: 9
2 2 a b
0 false
OperationBedeutung
ch <- xsenden
x := <-chempfangen
x, ok := <-chempfangen, ok == false wenn geschlossen und leer
close(ch)schließen (nur der Sender!)
for x := range chbis zum Schließen lesen
chan<- T, <-chan Tnur senden / nur empfangen (Typen in Signaturen)

Worker-Pool

Ein klassisches Muster: Viele Aufgaben, wenige Worker.

package main
 
import (
    "fmt"
    "sort"
    "sync"
)
 
func worker(id int, aufgaben <-chan int, ergebnisse chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for n := range aufgaben {
        ergebnisse <- n * n
    }
}
 
func main() {
    aufgaben := make(chan int, 10)
    ergebnisse := make(chan int, 10)
    var wg sync.WaitGroup
 
    for w := 1; w <= 3; w++ {
        wg.Add(1)
        go worker(w, aufgaben, ergebnisse, &wg)
    }
    for n := 1; n <= 9; n++ {
        aufgaben <- n
    }
    close(aufgaben)
 
    go func() { wg.Wait(); close(ergebnisse) }() // schließen, wenn alle Worker fertig sind
 
    var liste []int
    for r := range ergebnisse {
        liste = append(liste, r)
    }
    sort.Ints(liste)
    fmt.Println(liste)
}

Ausgabe:

[1 4 9 16 25 36 49 64 81]

select: auf mehrere Channels warten

package main
 
import (
    "fmt"
    "time"
)
 
func main() {
    a := make(chan string)
    b := make(chan string)
    go func() { time.Sleep(30 * time.Millisecond); a <- "von A" }()
    go func() { time.Sleep(10 * time.Millisecond); b <- "von B" }()
 
    for i := 0; i < 2; i++ {
        select {
        case m := <-a:
            fmt.Println(m)
        case m := <-b:
            fmt.Println(m)
        case <-time.After(time.Second):
            fmt.Println("Zeitüberschreitung")
        }
    }
 
    ch := make(chan int)
    select {
    case v := <-ch:
        fmt.Println(v)
    default: // wenn nichts bereit ist: nicht blockieren
        fmt.Println("nichts bereit")
    }
}

Ausgabe:

von B
von A
nichts bereit

Gemeinsame Daten schützen

Wenn mehrere Goroutinen dieselbe Variable ändern, entsteht ein Datenwettlauf. Lösungen:

package main
 
import (
    "fmt"
    "sync"
    "sync/atomic"
)
 
func main() {
    var mu sync.Mutex
    var geschuetzt int
    var atomar atomic.Int64
    var wg sync.WaitGroup
 
    for i := 0; i < 4; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for k := 0; k < 10000; k++ {
                mu.Lock()
                geschuetzt++
                mu.Unlock()
                atomar.Add(1)
            }
        }()
    }
    wg.Wait()
    fmt.Println(geschuetzt, atomar.Load())
}

Ausgabe:

40000 40000
WerkzeugZweck
sync.Mutex, sync.RWMutexgegenseitiger Ausschluss
sync/atomicatomare Zähler und Flags
sync.WaitGroupauf mehrere Goroutinen warten
sync.Oncegenau einmal ausführen
sync.Mapthreadsichere Map
ChannelsDaten übergeben statt teilen

Datenwettläufe findest du mit go run -race main.go oder go test -race.

context: Abbruch und Zeitlimits

package main
 
import (
    "context"
    "fmt"
    "time"
)
 
func langeArbeit(ctx context.Context) error {
    for i := 0; ; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err() // context.Canceled oder context.DeadlineExceeded
        case <-time.After(20 * time.Millisecond):
        }
    }
}
 
func main() {
    ctx, abbrechen := context.WithTimeout(context.Background(), 70*time.Millisecond)
    defer abbrechen()
    fmt.Println(langeArbeit(ctx))
 
    ctx2, stop := context.WithCancel(context.Background())
    go func() { time.Sleep(30 * time.Millisecond); stop() }()
    fmt.Println(langeArbeit(ctx2))
}

Ausgabe:

context deadline exceeded
context canceled

Gib jeder Funktion, die lange läuft oder Netzwerk benutzt, als ersten Parameter einen ctx context.Context.

Regeln

  • Wer startet, sorgt für das Ende: Jede Goroutine braucht einen klaren Abbruch (Channel, context)
  • Nur der Sender schließt einen Channel
  • Goroutinen nicht unbegrenzt starten (Worker-Pool oder errgroup)
  • Gemeinsamen Zustand vermeiden; wenn nötig Mutex/atomic
  • -race immer in Tests laufen lassen

Merke

  • go f() startet eine Goroutine; sync.WaitGroup wartet
  • Channels (chan T) übertragen Werte; close, range, select
  • Datenwettläufe vermeiden mit Mutex, atomic oder Channels; Test mit -race
  • context für Abbruch und Zeitlimits
  • Goroutinen sind billig, aber nicht kostenlos: begrenzen und beenden

Übungsaufgabe

Berechne mit einem Worker-Pool aus vier Goroutinen die Summe der Quadrate von 1 bis 1000.

Quiz zur Selbstkontrolle

Weiter im Kurs

Zurück: Interfaces und Generics

Weiter: Pakete, Module und Standardbibliothek

Alle Kapitel: Go im Überblick