1. Parallelität vs. Nebenläufigkeit

[THEORIE]

Du kennst Coroutines bereits: launch, async, suspend-Funktionen. Dieser Kurs geht einen Schritt tiefer und schaut sich an, was darunter tatsächlich passiert — auf der JVM, mit echten Threads. Bevor es dorthin geht, brauchst du zwei Begriffe, die im Alltag oft synonym benutzt werden, aber unterschiedliche Dinge beschreiben: Parallelität und Nebenläufigkeit.

Parallelität (parallelism) bedeutet: Mehrere Berechnungen laufen im selben Moment wirklich gleichzeitig — das erfordert mehrere CPU-Kerne, die tatsächlich zur selben Zeit rechnen.

Nebenläufigkeit (concurrency) bedeutet etwas Schwächeres: Mehrere Berechnungen machen in überlappenden Zeiträumen Fortschritt, statt streng nacheinander abzulaufen. Das schließt echte Gleichzeitigkeit ein, verlangt sie aber nicht. Ein einzelner CPU-Kern kann nebenläufig sein, indem er schnell zwischen mehreren Aufgaben hin- und herwechselt (Interleaving) — ohne dass jemals zwei Aufgaben im exakt selben Moment aktiv sind.

Damit gilt: Jede parallele Ausführung ist auch nebenläufig, aber nicht jede nebenläufige Ausführung ist parallel. Ein Single-Core-System kann nebenläufig sein (Task-Switching), aber nie parallel.

import kotlinx.coroutines.*

fun main() = runBlocking {
    repeat(4) { i ->
        launch(Dispatchers.Default) {
            println("Coroutine $i auf ${Thread.currentThread().name}")
        }
    }
}

Was hier passiert, hängt von der Hardware ab: Dispatchers.Default verteilt Arbeit auf einen Thread-Pool, dessen Größe sich an der Anzahl verfügbarer CPU-Kerne orientiert. Auf einer Vier-Kern-Maschine können mehrere dieser vier Coroutines tatsächlich parallel laufen. Auf einer Maschine mit nur einem Kern laufen sie nebenläufig, aber nacheinander interleaved — nie wirklich gleichzeitig.

Die Motivation, sich mit beidem zu beschäftigen, ist meist eine von zwei Fragen: Soll eine Aufgabe schneller werden, indem sie auf mehrere Kerne verteilt wird (Parallelität)? Oder soll eine Anwendung reaktionsfähig bleiben, während sie auf etwas Langsames wartet — eine Netzwerkantwort, eine Festplatte, einen Timer — ohne dabei blockiert zu sein (Nebenläufigkeit)? Coroutines sind in Kotlin das Werkzeug für Letzteres; ob dabei am Ende auch echte Parallelität stattfindet, hängt vom gewählten Dispatcher und der verfügbaren Hardware ab. Die folgenden Lektionen klären, was ein Dispatcher dafür konkret tut — angefangen bei dem, was er verteilt: Threads.

Weiterlesen

[VORHERSAGE]

Lies den Code genau durch, bevor du ihn ausführst.

vorhersage.kt
import kotlinx.coroutines.*

fun main() = runBlocking {
    launch(Dispatchers.Default) { println("A") }
    launch(Dispatchers.Default) { println("B") }
}

Gibt dieses Programm bei jedem Lauf garantiert zuerst A und dann B aus?

[AUFGABE]

Ein kleiner Bestell-Service soll für jede eingehende Bestellung eine eigene Coroutine starten, die meldet, welcher Thread sie bearbeitet. Ergänze die forEach-Schleife: Starte für jede Bestellung eine Coroutine mit launch(Dispatchers.Default), die "$bestellung läuft auf ${Thread.currentThread().name}" ausgibt.

aufgabe.kt
import kotlinx.coroutines.* fun main() = runBlocking { val bestellungen = listOf("Bestellung A", "Bestellung B", "Bestellung C") bestellungen.forEach { bestellung -> // TODO: launch(Dispatchers.Default) { ... } mit Ausgabe von bestellung und Thread-Name } }

Bevor du einen Hint bekommst: Was hast du schon probiert, und woran hängt es genau?

[VERSTÄNDNIS-CHECK]

1. Wie unterscheidet die Lektion Parallelität und Nebenläufigkeit?

2. Ein Programm mit zwei Coroutines läuft auf einer Maschine mit nur einem CPU-Kern. Ist das möglich?

3. Was war laut dieser Lektion die Motivation, überhaupt parallel oder nebenläufig zu programmieren?