launch/join vs async/await in Kotlin: Was wählen im Jahr 2025? Vollständige Analyse mit Beispielen
\n\nHallo, Kollege! Wenn du jemals Kotlin-Coroutinen geschrieben hast, hast du dich sicherlich gefragt: "Was ist eigentlich der Unterschied zwischen launch und async? Und wozu braucht man dieses mysteriöse join?". Diese Frage ist ein echter Hit auf Stack Overflow und hat tausende Aufrufe. Und das nicht ohne Grund: Auf den ersten Blick tun beide Werkzeuge dasselbe – sie starten eine Hintergrundaufgabe. Aber der Teufel steckt, wie immer, im Detail.
Im Jahr 2025, wenn Kotlin coroutines zum Mainstream in der Android-Entwicklung und im Backend geworden sind, ist das Verständnis dieser Unterschiede keine bloße Marotte, sondern eine Notwendigkeit, um sauberen, sicheren und leistungsfähigen Code zu schreiben. In diesem Artikel werden wir nicht nur trockene Fakten auflisten. Wir werden reale Szenarien analysieren, uns den Code ansehen und Best Practices ableiten, die du sofort in deinen Projekten anwenden kannst.
\n\nLos geht's!
\n\nlaunch: "Starten und vergessen"
\n\nDie Funktion launch ist wohl der einfachste Weg, eine Coroutine zu starten. Sie erstellt eine neue Coroutine, die kein Ergebnis zurückgibt. Ihre Aufgabe ist es, eine bestimmte Arbeit zu erledigen: UI aktualisieren, ein Log senden, Daten in eine Datei schreiben. Der Rückgabetyp ist Job. Es ist, als würde man einem Kurier einen Auftrag geben: "Liefere das Paket und komm zurück, kein Bericht nötig".
Die Methode join ermöglicht es, auf das Ende dieser Coroutine zu warten. Sie unterbricht die Ausführung der aktuellen Coroutine, bis der Job, der join aufgerufen hat, abgeschlossen ist.
Betrachten wir ein Beispiel. Angenommen, wir müssen parallel zwei Bilder laden und sie dann kombinieren. Mit launch und join würde das so aussehen:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n // Ladevorgang simulieren\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithLaunch() = coroutineScope {\n var image1: Image? = null\n var image2: Image? = null\n\n val job1 = launch {\n image1 = loadImage("https://example.com/img1.png")\n }\n\n val job2 = launch {\n image2 = loadImage("https://example.com/img2.png")\n }\n\n // Auf beide Aufgaben warten\n job1.join()\n job2.join()\n\n // Jetzt sind image1 und image2 garantiert geladen\n println("Images loaded: $image1 and $image2")\n}\n\nWas ist hier wichtig? Wir verwenden veränderliche Variablen (var), um das Ergebnis zu speichern. Das ist nicht sehr elegant und kann zu Problemen mit Threads führen, obwohl Coroutinen innerhalb eines Threads arbeiten. Der Hauptnachteil ist jedoch, dass wir launch nicht direkt zur Rückgabe eines Werts verwenden können.
async: "Starten und Ergebnis erhalten"
\n\nAber async ist bereits die "Business-Class". Es erstellt eine Coroutine, die ein Ergebnis in Form eines Deferred<T>-Objekts zurückgibt. Es ist, als würde man ein Taxi bestellen: Man wartet, und einem wird nicht nur die Tatsache der Fahrt zurückgegeben, sondern ein konkretes Ergebnis – man ist am Ziel. Um dieses Ergebnis zu erhalten, wird await() verwendet.
Das gleiche Beispiel mit dem Laden von Bildern, aber mit async/await, wird eleganter:
import kotlinx.coroutines.*\n\nsuspend fun loadImage(url: String): Image {\n delay(1000)\n return Image(url)\n}\n\ndata class Image(val url: String)\n\nsuspend fun loadTwoImagesWithAsync() = coroutineScope {\n val deferred1 = async { loadImage("https://example.com/img1.png") }\n val deferred2 = async { loadImage("https://example.com/img2.png") }\n\n // await() unterbricht die Coroutine, bis das Ergebnis vorliegt\n val image1 = deferred1.await()\n val image2 = deferred2.await()\n\n println("Images loaded: $image1 and $image2")\n}\n\nSpürst du den Unterschied? Kein var, keine manuelle Verwaltung. Alles sauber,