DEV Community

Cover image for Goroutines vs. OS-Threads in Go: Einfacher Leitfaden für Anfänger
Amin Mohammadi
Amin Mohammadi

Posted on

Goroutines vs. OS-Threads in Go: Einfacher Leitfaden für Anfänger

Wenn du gerade mit Go anfängst, hörst du oft von Goroutines – aber was sind sie eigentlich? Und warum sind sie anders als normale OS Threads (Betriebssystem-Threads)?

In diesem Artikel erklären wir dir:

  • Was OS Threads sind (ganz einfach)
  • Was Goroutines sind
  • Warum Goroutines in Go so beliebt sind
  • Praktische Beispiele zum Ausprobieren

Alles auf einem Anfänger-Level, damit du direkt loslegen kannst!


1. Was sind OS Threads? (Die "Klassischen" Threads)

Ein OS Thread (Operating System Thread) ist eine Einheit, die vom Betriebssystem verwaltet wird. Jeder Thread hat:

  • Seinen eigenen Stack (Speicherbereich für lokale Variablen)
  • Seinen eigenen Programmzähler (wo im Code er gerade ist)
  • Wird vom Betriebssystem geplant (Scheduler)

Ein Einfaches Beispiel mit OS Threads in Go

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    // Wie viele OS Threads Go verwenden kann
    fmt.Printf("Anzahl OS Threads: %d\n", runtime.GOMAXPROCS(0))

    var wg sync.WaitGroup

    // Wir starten 5 OS Threads
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Thread %d läuft\n", id)
            time.Sleep(1 * time.Second)
        }(i)
    }

    wg.Wait()
    fmt.Println("Alle Threads fertig!")
}
Enter fullscreen mode Exit fullscreen mode

Was passiert hier?

  • runtime.GOMAXPROCS(0) zeigt dir, wie viele echte OS Threads Go verwenden kann
  • Standardmäßig ist das die Anzahl deiner CPU-Kerne
  • Jede go-Funktion läuft auf einem OS Thread

Problem: OS Threads sind schwer (heavyweight):

  • Jeder Thread braucht viel Speicher (meist 1-2 MB pro Thread)
  • Das Betriebssystem muss sie planen (Scheduling) – das kostet Zeit
  • Du kannst nicht Tausende von OS Threads gleichzeitig haben

2. Was sind Goroutines? (Die "Leichten" Threads von Go)

Eine Goroutine ist eine leichte Funktion, die parallel laufen kann. Sie wird nicht vom Betriebssystem, sondern von Go selbst verwaltet.

Ein Einfaches Beispiel mit Goroutines

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    // Wir starten 1000 Goroutines – das wäre mit OS Threads unmöglich!
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Goroutine %d läuft\n", id)
            time.Sleep(100 * time.Millisecond)
        }(i)
    }

    wg.Wait()
    fmt.Println("Alle Goroutines fertig!")
}
Enter fullscreen mode Exit fullscreen mode

Was ist der Unterschied?

  • Goroutines sind super leicht: Nur 2-8 KB Speicher pro Goroutine
  • Go verwaltet sie selbst: Der Go-Scheduler teilt OS Threads zwischen Goroutines auf
  • Du kannst Millionen starten: Praktisch keine Grenze!

3. Der Wichtigste Unterschied: Speicherverbrauch

Lass uns das praktisch testen:

3.1. Test: Wie Viele OS Threads Können Wir Starten?

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    var mu sync.Mutex
    var count int

    // Versuchen wir, 10.000 "Threads" zu starten
    // (In Go sind das eigentlich Goroutines, die auf OS Threads laufen)
    for i := 0; i < 10000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            time.Sleep(1 * time.Second)
            mu.Lock()
            count++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Printf("Anzahl fertiger Goroutines: %d\n", count)
    fmt.Printf("Anzahl OS Threads verwendet: %d\n", runtime.GOMAXPROCS(0))
}
Enter fullscreen mode Exit fullscreen mode

Ergebnis:

  • Du kannst 10.000 Goroutines starten
  • Aber Go verwendet nur 4-8 OS Threads (je nach CPU)
  • Der Go-Scheduler teilt die Goroutines auf diese Threads auf

4. Wie Funktioniert der Go-Scheduler?

Der Go-Scheduler ist wie ein Manager, der entscheidet, welche Goroutine wann läuft:

OS Thread 1: [Goroutine A] → [Goroutine B] → [Goroutine C] → ...
OS Thread 2: [Goroutine D] → [Goroutine E] → [Goroutine F] → ...
OS Thread 3: [Goroutine G] → [Goroutine H] → [Goroutine I] → ...
Enter fullscreen mode Exit fullscreen mode

Wie funktioniert das?

  1. Eine Goroutine läuft auf einem OS Thread
  2. Wenn sie blockiert (z.B. auf I/O wartet), gibt sie den Thread frei
  3. Eine andere Goroutine kann dann auf diesem Thread laufen
  4. So teilen sich Tausende Goroutines nur wenige OS Threads

Beispiel: Blockierende Operationen

package main

import (
    "fmt"
    "net/http"
    "sync"
    "time"
)

func fetchURL(url string, wg *sync.WaitGroup) {
    defer wg.Done()

    // Diese Operation blockiert (wartet auf Netzwerk)
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Fehler bei %s: %v\n", url, err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("URL %s geladen: Status %d\n", url, resp.StatusCode)
}

func main() {
    var wg sync.WaitGroup
    urls := []string{
        "https://www.google.com",
        "https://www.github.com",
        "https://www.stackoverflow.com",
    }

    // Alle URLs werden parallel geladen
    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, &wg)
    }

    wg.Wait()
    fmt.Println("Alle URLs geladen!")
}
Enter fullscreen mode Exit fullscreen mode

Was passiert hier?

  • Jede fetchURL-Goroutine blockiert, während sie auf die HTTP-Antwort wartet
  • Während sie wartet, kann eine andere Goroutine auf demselben OS Thread laufen
  • So können wir viele Netzwerk-Anfragen parallel machen, ohne viele OS Threads zu brauchen

5. Praktischer Vergleich: Performance

Lass uns ein praktisches Beispiel machen, das den Unterschied zeigt:

5.1. Beispiel: Viele Zahlen Addieren

package main

import (
    "fmt"
    "sync"
    "time"
)

// Funktion, die etwas "Arbeit" macht
func calculateSum(start, end int, result *int, wg *sync.WaitGroup) {
    defer wg.Done()
    sum := 0
    for i := start; i < end; i++ {
        sum += i
        // Simuliere etwas Arbeit
        time.Sleep(1 * time.Millisecond)
    }
    *result = sum
}

func main() {
    start := time.Now()
    var wg sync.WaitGroup
    results := make([]int, 100)

    // Starte 100 Goroutines
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go calculateSum(i*100, (i+1)*100, &results[i], &wg)
    }

    wg.Wait()
    elapsed := time.Since(start)

    total := 0
    for _, r := range results {
        total += r
    }

    fmt.Printf("Gesamtsumme: %d\n", total)
    fmt.Printf("Zeit: %v\n", elapsed)
}
Enter fullscreen mode Exit fullscreen mode

Warum ist das schnell?

  • 100 Goroutines laufen parallel
  • Sie teilen sich nur 4-8 OS Threads
  • Der Go-Scheduler wechselt schnell zwischen ihnen
  • Viel schneller als wenn wir alles nacheinander machen würden!

6. Wann Verwende Ich Was?

OS Threads (Direkt in Go selten nötig)

Du musst selten direkt mit OS Threads arbeiten in Go, weil:

  • Goroutines sind einfacher zu verwenden
  • Sie sind effizienter
  • Go verwaltet sie automatisch

Aber: Manchmal brauchst du OS Threads für:

  • C-Bibliotheken, die Threads erwarten
  • Sehr spezielle System-Programmierung

Goroutines (Das Standard-Tool in Go)

Verwende Goroutines für:

  • Parallele Netzwerk-Anfragen (HTTP, Datenbanken)
  • Parallele Berechnungen
  • Concurrent Programming (mehrere Dinge gleichzeitig)
  • Worker Pools (viele Aufgaben parallel abarbeiten)

7. Ein Praktisches Beispiel: Worker Pool

Ein Worker Pool ist ein Muster, bei dem du eine begrenzte Anzahl von Goroutines hast, die viele Aufgaben abarbeiten:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("Worker %d bearbeitet Job %d\n", id, job)
        // Simuliere Arbeit
        time.Sleep(500 * time.Millisecond)
        results <- job * 2 // Ergebnis zurückgeben
    }
}

func main() {
    const numWorkers = 3  // Nur 3 Worker (Goroutines)
    const numJobs = 10    // 10 Jobs zu erledigen

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    // Starte Worker (Goroutines)
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // Sende Jobs
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Warte auf Ergebnisse
    go func() {
        wg.Wait()
        close(results)
    }()

    // Zeige Ergebnisse
    for result := range results {
        fmt.Printf("Ergebnis: %d\n", result)
    }
}
Enter fullscreen mode Exit fullscreen mode

Was passiert hier?

  • Wir haben nur 3 Worker-Goroutines
  • Sie bearbeiten 10 Jobs nacheinander
  • Jede Goroutine läuft auf einem OS Thread (geteilt)
  • Viel effizienter als 10 separate OS Threads!

8. Häufige Fehler und Tipps

Fehler 1: Goroutines ohne Synchronisation

// ❌ FALSCH
func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i) // Problem: i kann sich ändern!
        }()
    }
    time.Sleep(1 * time.Second) // Nicht zuverlässig!
}
Enter fullscreen mode Exit fullscreen mode
// ✅ RICHTIG
func main() {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Println(id) // id ist eine Kopie
        }(i)
    }
    wg.Wait() // Warte auf alle Goroutines
}
Enter fullscreen mode Exit fullscreen mode

Fehler 2: Zu Viele Goroutines

// ❌ FALSCH: Millionen Goroutines starten
for i := 0; i < 10000000; i++ {
    go doSomething()
}
Enter fullscreen mode Exit fullscreen mode
// ✅ RICHTIG: Worker Pool verwenden
const maxWorkers = 100
jobs := make(chan int, 1000)

for w := 0; w < maxWorkers; w++ {
    go worker(jobs)
}
Enter fullscreen mode Exit fullscreen mode

9. Zusammenfassung: Die Wichtigsten Unterschiede

Feature OS Thread Goroutine
Speicher 1-2 MB 2-8 KB
Verwaltung Betriebssystem Go Runtime
Anzahl Hunderte Millionen möglich
Scheduling OS Scheduler Go Scheduler
Geschwindigkeit Langsamer (mehr Overhead) Schneller (weniger Overhead)

Einfach gesagt:

  • OS Threads = Schwere Lastwagen (viel Kraft, aber teuer)
  • Goroutines = Leichte Motorräder (schnell, effizient, viele gleichzeitig)

10. Nächste Schritte

Jetzt, wo du den Unterschied verstehst, kannst du:

  1. Mehr über Channels lernen (Kommunikation zwischen Goroutines)
  2. Select-Statements ausprobieren (mehrere Channels gleichzeitig)
  3. Context-Package verwenden (Goroutines abbrechen)
  4. Sync-Package erkunden (Mutex, WaitGroup, etc.)

Praktische Übungen:

  • Baue einen einfachen Web-Scraper mit Goroutines
  • Erstelle einen Worker Pool für Datei-Verarbeitung
  • Implementiere parallele API-Aufrufe

Fazit

  • Goroutines sind die "Superkräfte" von Go
  • Sie sind viel leichter als OS Threads
  • Du kannst viele gleichzeitig verwenden
  • Der Go-Scheduler verwaltet sie automatisch
  • Perfekt für parallele Programmierung!

Denk daran: In Go verwendest du fast immer Goroutines, nicht direkt OS Threads. Go macht das für dich!


Weitere Ressourcen

Top comments (0)