Kotlin Coroutinen gehen über die reine Nebenläufigkeit hinaus
1. Oktober 202314 Min. LesezeitAktualisiert 22. Februar 2026
Jedes Mal, wenn Sie von Kotlin Coroutinen hören, denken Sie wahrscheinlich an eine einfache, prägnante und leistungsstarke Lösung für die Bearbeitung asynchroner Aufgaben, wie zum Beispiel Netzwerkanfragen. Aber ist das ihr einziger Zweck? Betrachten wir die Verwendung von Kotlin Coroutinen jenseits der Nebenläufigkeit.
Kotlin Coroutinen Primitive
Beginnen wir damit, zu verstehen, wie der zugrunde liegende Mechanismus von Coroutinen funktioniert. Wenn wir uns die Kotlin Coroutinen Primitive ansehen, handelt es sich nur um ein paar Klassen und Funktionen:
ContinuationCoroutineContextsuspendCoroutinecreateCoroutinestartCoroutine
Das ist alles, was wir in unserem
kotlin-stdlib haben. Aber wofür werden sie verwendet? Tauchen wir tiefer in die Funktionsweise von Kotlin Coroutinen ein.Continuation
Continuation ist lediglich ein Interface mit zwei Mitgliedern:
context und resumeWith:kotlin
Was tut es und welchen Zweck hat es?
Continuation – ist wörtlich die primäre Coroutine. Es ist wie ein verbesserter Callback mit der Fähigkeit, zusätzliche Informationen zu verbreiten und ein nützliches Ausführungsergebnis an die Coroutine zu liefern.Wir haben noch nicht über
CoroutineContext gesprochen, betrachten wir vorerst nur resumeWith.resumeWith
Wie jeder andere Callback ist dies eine Funktion, die aufgerufen wird, wenn die Coroutine ihre Arbeit beendet. Sie verwendet
kotlin.Result, um alle in der Coroutine auftretenden Ausnahmen sicher weiterzuleiten.Wir können also unsere Nebenläufigkeitslogik mithilfe von
Continuation auf folgende Weise erstellen:kotlin
suspendCoroutine– ist eine Brücke zwischen Kotlin Coroutinen-Code und nicht-Coroutinen-basiertem Code.
Oder verwenden Sie eine bestehende asynchrone (Pseudocode):
kotlin
Continuation<in T>.resume(..)ist die Erweiterung, um die Übergabe vonkotlin.Resultjedes Mal zu vermeiden.
Wir können also nicht nur unsere Nebenläufigkeitslogik implementieren, sondern auch bestehende nutzen und sie mit Kotlin Coroutinen zum Laufen bringen.
startCoroutine
Wir können auch Suspend-Funktionen aus nicht-Suspend-Kontexten mit
startCoroutine starten. In Kotlin wird dies immer am Ende verwendet, wenn Ihre main-Funktion suspend ist.kotlinx.coroutinesverwendet es auch, um Coroutinen auszuführen, aber der Mechanismus dort ist natürlich viel komplexer.
kotlin
Aber natürlich können Sie Suspend-Funktionen auskotlinx.coroutinesnicht einfach auf diese Weise aufrufen, wenn Sie Coroutinen ausführen.
CoroutineContext
Jetzt kommen wir zu einem weiteren Mitglied von
Continuation – CoroutineContext. Wozu dient es?CoroutineContext ist ein Provider, der die benötigten Daten an die Coroutine weitergibt. In der realen Welt geht es dabei normalerweise um die Übergabe von Parametern über eine komplexe Kette von Coroutinen hinweg.Um es klarer auszudrücken, CoroutineContext inkotlinx.coroutinessteht für strukturierte Nebenläufigkeit.
Einfaches Beispiel
Erstellen wir aus unserem vorherigen Beispiel für
startCoroutine Code mit der Fähigkeit, Werte aus CoroutineContext abzurufen:kotlin
CoroutineContext.Element– ist der Abstrakt, der zum Speichern von Elementen inCoroutineContextverwendet wird.CoroutineContext.Key– ist der Bezeichner vonCoroutineContext.Element.
Sie können diesen Code hier ausprobieren.
Beispiel aus der Praxis
Stellen wir uns vor, wir haben unseren API-Dienst. Normalerweise benötigen wir eine Autorisierungsebene, daher betrachten wir das nächste Beispiel (als solches habe ich gRPC genommen):
kotlin
CoroutineContext in kotlinx.coroutines ist buchstäblich nur eine Map<K : CoroutineContext.Key, V : CoroutineContext.Element> (genauer gesagt, es ist eine ConcurrentHashMap auf der JVM, zum Beispiel), genau wie in unserem obigen Beispiel. Aber, wenn wir über kotlinx.coroutines sprechen, wird es an alle Kind-Coroutinen innerhalb der gewünschten Coroutine weitergegeben (einen solchen Mechanismus hatten wir nicht).So können wir es jetzt in Kind-Coroutinen abrufen:
kotlin
Interessante Tatsache: coroutineContext ist die einzige Eigenschaft in Kotlin, die densuspend-Modifikator hat. 👀
Für gRPC müssen wir auch unseren Interceptor registrieren und unsere RPCs schreiben. Aber die Idee dieser Lösung für gRPC ist einfach – Logik entkoppeln und die Entwicklererfahrung vereinfachen.
Für Java verwendet gRPC ThreadLocal, daher können wirCoroutineContextauch als Alternative zuThreadLocalbetrachten. Wir könnenThreadLocalinnerhalb von Coroutinen nicht verwenden, da eine Coroutine normalerweise nicht mit einem bestimmten Thread verknüpft ist (insbesondere, wenn wir überwithContextsprechen). Coroutinen werden eher auf einem anderen Thread fortgesetzt (außerdem können verschiedene Coroutinen auf einem einzigen Thread ausgeführt werden).
Aber bedeutet das nicht, dass der einzige Grund, warum Coroutinen existieren, die Nebenläufigkeit ist? Lassen Sie es mich erklären.
Sequence
Eines der häufigsten Beispiele ist die –
kotlin.sequences.Sequence<T>. Kurz gesagt, es ist eine faule Sammlung, die nur dann iteriert, wenn Sie Elemente konsumieren. Sie können mehr darüber hier lesen.Wenn Sie jemals die
SequenceScope-Quellen betrachtet haben, verwendet sie unter der Haube Suspend-Funktionen:kotlin
@RestrictSuspensionverbietet Konsumenten, nicht-Mitglieds-Suspend-Funktionen aufzurufen.
Die Idee ist also, dass Elemente faul konsumiert werden. Sie können sie als reguläre Sammlungen verwenden und die Vorteile der faulen Iteration nutzen.
Aber wie funktioniert es unter der Haube? Werfen wir einen Blick auf die Implementierungsquellen:
kotlin
COROUTINE_SUSPENDEDist eine spezielle Konstante, die intern vom Kotlin-Compiler verwendet wird, um die Suspendierung und Wiederaufnahme von Coroutinen zu verwalten. Es ist nichts, womit Entwickler normalerweise direkt interagieren, sondern dient als internes Signal innerhalb des Coroutinen-Mechanismus.
Sieht etwas schwierig zu lesen aus, nicht wahr? Gehen wir Schritt für Schritt vor:
- Zuerst beginnen wir mit Zuständen. Wir haben die nächsten Zustände, lassen Sie uns kurz darüber sprechen:
- State_NotReady: Der Iterator ist derzeit nicht bereit, ein Element bereitzustellen. Er wartet möglicherweise auf eine Operation oder weitere Verarbeitung, um ein Element verfügbar zu machen.
- State_ManyNotReady: Der Iterator ist darauf vorbereitet, mehrere Elemente bereitzustellen, aber sie sind nicht sofort verfügbar. Er wartet auf ein Signal, dass Elemente zum Verbrauch bereit sind (im Grunde wartet er auf einen Terminaloperator).
- State_ManyReady: Der Iterator ist jetzt bereit, mehrere Elemente bereitzustellen. Er kann sofort das nächste Element aus der Sequenz liefern.
- State_Ready: Der Iterator hat ein einzelnes Element bereit, das bereitgestellt werden kann. Er ist darauf eingestellt, das Element sofort auf Anfrage zu liefern.
- State_Done: Der Iterator hat keine weiteren Elemente mehr bereitzustellen. Er hat seine Aufgabe, Elemente aus der Sequenz zu erzeugen, abgeschlossen. Diesen Zustand erreichen wir, wenn wir
SequenceBuilderverlassen. - State_Failed: Etwas Unerwartetes ist passiert, und der Iterator ist auf ein Problem gestoßen. Normalerweise sollte dies nicht passieren.
hasNextgibt je nach Zustand einen Wert oder eine Reihe von Werten zurück, wenn er bereit ist, zu konsumieren. Darüber hinaus startet er die Ausführung der Sequenz bei jeder Iteration innerhalb vonwhile. Wenn alsoState_NotReadyvorliegt, wird es durch Ausführen der nächsten Yields bereit gemacht.- Die Funktion
nextruft das nächste Element aus dem Iterator basierend auf seinem aktuellen Zustand ab (ähnlich wie beihasNext). WennnextohnehasNextaufgerufen wurde, können SienextNotReady()erreichen. In anderen Situationen wird einfach der Wert zurückgegeben. - Die
yield-Funktion ändert lediglich die Zustände der Sequenziterator-Implementierung. Wenn neue Elemente hinzugefügt werden, wechselt sie zuState_Ready. Die Verwendung vonsuspendCoroutineUninterceptedOrReturnsuspendiert die Coroutine (Ausführung) und setzt sie später fort. Sie wird gestartet, wenn die vorherige Coroutine (Suspend-Punkt) beendet ist.
Um meine Erklärung abzuschließen, beenden wir sie einfach damit, wie wir die gleiche Funktionalität nur mit Callbacks realisieren könnten:
kotlin
Aber es sieht etwas schwierig zu lesen aus, nicht wahr? Deshalb sind Coroutinen in dieser speziellen Situation nützlich.
Am Ende sieht es doch gar nicht so komplex aus, oder?
DeepRecursiveScope
Nun wollen wir uns einen weiteren Anwendungsfall für Kotlin Coroutinen ansehen –
DeepRecursiveScope. Wie Sie wahrscheinlich wissen, haben wir normalerweise, wenn eine bestimmte Funktion sich selbst aufruft, eine Wahrscheinlichkeit, einen StackOverflowError zu erhalten, da jeder Aufruf dazu beiträgt, unseren Stack zu füllen.Zu diesem Zweck existiert zum Beispiel auch die Sprachkonstruktiontailrec. Der Unterschied ist, dasstailreckeine Verzweigungen (bedingte Prüfungen) mit Aufrufen anderer Funktionen haben kann.Sie können mehr darüber hier lesen.
DeepRecursiveScope stützt sich also nicht auf den traditionellen Stack-Flow, sondern nutzt alle Funktionen, die Coroutinen bieten. Um es besser zu verstehen, betrachten wir das klassische Beispiel mit Fibonacci-Zahlen:kotlin
Für ein komplexeres Beispiel können Sie die kdoc konsultieren.
Wir werden uns nicht mit den genauen Implementierungsdetails von
DeepRecursiveScope aufhalten (Sie können sie hier nachlesen), da sie die gleiche Idee wie Sequence mit zusätzlichem Verhalten zur Unterstützung der bereitgestellten Mechanismen haben, aber lassen Sie uns diskutieren, wie Kotlin Coroutinen dieses spezielle Problem lösen. Außerdem gibt es einen sehr guten Artikel darüber von Roman Elizarov.Coroutinen intern
Wie genau löst es das Problem? Wie ich bereits erwähnt habe, sind Coroutinen von CPS (Continuation Passing Style) inspiriert, aber es ist nicht genau das, was der Kotlin-Compiler tut, um Coroutinen so effizient zu handhaben.
Der Kotlin-Compiler verwendet eine Kombination von Optimierungen, um den Coroutinen-Stack und die Ausführung effizient zu verwalten. Schauen wir uns an, was genau er tut:
- Compiler-Transformationen: Der Kotlin-Compiler generiert einen Zustandsautomaten, ähnlich dem, was wir in den Implementierungsdetails von Sequenzen gesehen haben. Es beseitigt nicht alle Stack-Aufrufe, aber reduziert sie ausreichend, um keinen
StackOverflowErrorzu erhalten. - Heap-Allokation von Continuations: In einer traditionellen Callback-Kette schiebt jeder Funktionsaufruf Daten auf den Call-Stack. Wenn die Kette tief ist, kann dies viel Stack-Speicher verbrauchen. Beim Coroutine-Ansatz wird, wenn eine Coroutine suspendiert wird, ihre Continuation als Objekt auf dem Heap gespeichert. Dieses Continuation-Objekt enthält die notwendigen Informationen (Stack, Dispatcher usw.), um die Ausführung der Coroutine fortzusetzen. Diese Heap-Speicherung ermöglicht eine viel größere Kapazität, tiefe Aufrufketten zu verarbeiten, ohne das Risiko eines Stack-Überlaufs.
Der genaue Mechanismus von Coroutinen ist der folgende:
- Serialisierung: Der Stack-Zustand der suspendierten Coroutine wird in einem Heap-allokierten Continuation-Objekt gespeichert.
- Wiederaufnahme: Wenn die Wiederaufnahme bereit ist, richtet das Framework einen nativen Stack ein, um den erfassten Zustand nachzubilden.
- Speicherkopie: Der serialisierte Stack-Zustand wird vom Continuation-Objekt in den nativen Stack kopiert.
- Kontextkonfiguration: Der Ausführungskontext wird an den ursprünglichen Zustand angepasst.
- Programmzeiger: Der Programmzeiger wird auf den gespeicherten Wert für die korrekte Anweisung gesetzt.
- Aufruf: Der Continuation-Code wird mit CPS aufgerufen, wodurch die Ausführung fortgesetzt wird.
Die Stack-Wiederherstellung hilft uns auch bei der Wiederaufnahme auf verschiedenen Threads, da diese nichts über den Stack unserer Coroutine wissen.
So können wir von nun an verstehen, wie Coroutinen intern funktionieren. Gehen wir zu anderen Beispielen über, in denen Kotlin Coroutinen über die Nebenläufigkeit hinaus verwendet werden.
Jetpack Compose
Wenn Sie jemals mit Compose gearbeitet und beispielsweise Zeigerereignisse verarbeitet haben, haben Sie wahrscheinlich bemerkt, dass einige Hacks aus Coroutinen verwendet werden, um auf Updates zu warten:
kotlin
Wie Sie sehen, ist der Scope, der zur Verarbeitung von Zeigerereignissen verwendet wird, mit
@RestrictsSuspension markiert. Wenn wir die bereitgestellte Dokumentation konsultieren, sehen wir Folgendes:markdown
awaitPointerEvent wird mit Kotlin-Primitiven verarbeitet, ohne kotlinx.coroutines. Da wir in einer solchen Situation keine kotlinx.coroutines-Logik benötigen (es ist im Grunde nur ein Callback, der nach einer Benutzeraktion vom Main-Looper-Thread aufgerufen wird).Fazit
Zusammenfassend hat dieser Artikel verschiedene Facetten von Kotlin Coroutinen beleuchtet und ihre Vielseitigkeit jenseits traditioneller Nebenläufigkeitsaufgaben hervorgehoben. Wir haben uns mit den inneren Mechanismen der Coroutinen-Primitive befasst, ihre Verwendung in Sequenzen und komplexen Problemlösungsszenarien wie tiefer Rekursion diskutiert und reale Beispiele untersucht, die ihre breite Anwendbarkeit demonstrieren. Der Titel "Coroutinen gehen über die reine Nebenläufigkeit hinaus" spiegelt treffend die vielfältigen Fähigkeiten wider, die Kotlin Coroutinen in der modernen Softwareentwicklung bieten.
Fühlen Sie sich frei, Ihr Fachwissen zwanglos in den Kaffeepausen-Chat einzubringen!
Das könnte Ihnen auch gefallen
22 Min. Lesezeit
Fehlschläge, die wir nicht korrekt modellieren
Warum das Zurückgeben von null, das Werfen von Exceptions oder das Wrappen in Result keine bloße Stilfrage ist – es ist ein Vertrag, den du definierst.
Notiz lesen
26 Min. Lesezeit
Semantische Typisierung, die wir ignorieren
Der Sprung von der 'Das ist ein String'-Mentalität zu 'Das ist ein Konzept'. Erfahren Sie, wie Sie mit semantischer Typisierung in Kotlin bessere, selbstdokumentierende Domänenmodelle erstellen.
Notiz lesen