Wie funktioniert die Unterbrechung in Kotlin-Coroutinen?
Dies ist ein übersetztes Kapitel aus dem Buc Kotlin Coroutines. Wenn Sie mir helfen möchten, die Übersetzung zu verbessern, finden Sie die Quellen auf GitHub.
Unterbrechbare Funktionen sind das Markenzeichen von Kotlin-Coroutinen. Die Fähigkeit, die Ausführung zu unterbrechen, ist das grundlegendste Merkmal, auf dem alle anderen Konzepte von Kotlin-Coroutinen aufbauen. Deshalb ist unser Ziel in diesem Kapitel, ein solides Verständnis dafür aufzubauen, wie es funktioniert.
Eine Koroutine zu unterbrechen bedeutet, sie mitten in der Ausführung anzuhalten. Es ähnelt dem Anhalten eines Videospiels: Sie speichern an einem Checkpoint, schalten das Spiel ab und sowohl Sie als auch Ihr Computer können sich auf andere Aufgaben konzentrieren. Wenn Sie später weitermachen möchten, schalten Sie das Spiel wieder ein, setzen es vom gespeicherten Checkpoint aus fort und können genau dort weiterspielen, wo Sie zuvor aufgehört hatten. Dies ist eine Analogie zu Coroutinen. Wenn sie unterbrochen sind, geben sie eine Continuation
zurück. Es ist wie ein Speichern in einem Spiel: wir können es verwenden, um an dem Punkt fortzufahren, an dem wir gestoppt haben.
Beachten Sie, dass dies sehr unterschiedlich von einem Thread ist, der nicht gespeichert, nur blockiert werden kann. Eine Koroutine ist viel leistungsfähiger. Wenn sie unterbrochen ist, verbraucht sie keine Ressourcen. Eine Koroutine kann auf einem anderen Thread fortgesetzt werden und (zumindest theoretisch) kann eine Continuation serialisiert, deserialisiert und dann fortgesetzt werden.
Resume
Lassen Sie uns das nun in Aktion sehen. Dafür brauchen wir eine Koroutine. Wir starten Coroutinen mit Coroutinen-Buildern (wie runBlocking
oder launch
), die wir später einführen werden. Es gibt allerdings auch einen einfacheren Weg: Wir können eine unterbrechbare main
Funktion verwenden.
Unterbrechbare Funktionen sind Funktionen, die eine Koroutine unterbrechen können. Das bedeutet, dass sie von einer Koroutine (oder einer anderen unterbrechbaren Funktion) aufgerufen werden müssen. Letztendlich müssen sie etwas zum Unterbrechen haben. Die Funktion main
ist der Startpunkt, daher wird Kotlin sie in einer Koroutine starten, wenn wir sie ausführen.
Dies ist ein einfaches Programm, das "Before" und "After" ausgibt. Was passiert, wenn wir zwischen diesen beiden Ausgaben pausieren? Dafür können wir die suspendCoroutine
Funktion verwenden, die uns die Standard Kotlin Bibliothek1 zur Verfügung stellt.
Wenn Sie den oben genannten Code aufrufen, werden Sie "After" nicht sehen, und der Code hört nicht auf zu laufen (da unsere main
Funktion nie abgeschlossen wurde). Die Koroutine wird nach "Before" ausgesetzt. Unser Programm wurde gestoppt und nie wieder aufgenommen. Also, wie können wir fortsetzen? Wo ist diese zuvor erwähnte Continuation
?
Betrachten Sie noch einmal den Aufruf von suspendCoroutine
und beachten Sie, dass er mit einem Lambda-Ausdruck ({ }
) endet. Die Funktion, die als Argument übergeben wird, wird bevor die Aussetzung stattfindet aufgerufen. Diese Funktion bekommt eine Kontinuation als Argument übergeben.
Dass eine Funktion eine andere Funktion an Ort und Stelle aufruft, ist nichts Ungewöhnliches. Dies ähnelt let
, apply
oder useLines
. Die Funktion suspendCoroutine
ist auf die gleiche Weise gestaltet, was es ermöglicht, die Kontinuation kurz vor der Aussetzung zu nutzen. Nach dem Aufruf von suspendCoroutine
wäre es zu spät. Daher wird der Lambda-Ausdruck, der als Parameter an die Funktion suspendCoroutine
übergeben wird, kurz vor der Aussetzung aufgerufen. Dieses Lambda wird dazu genutzt, diese Kontinuation irgendwo zu speichern oder zu planen, ob sie fortgesetzt werden soll.
Wir könnten es zur sofortigen Fortsetzung verwenden:
Beachten Sie, dass "After" im obigen Beispiel ausgegeben wird, weil wir resume
in suspendCoroutine
2 aufrufen.
Seit Kotlin 1.3 hat sich die Definition von
Continuation
geändert. Stattresume
undresumeWithException
gibt es eineresumeWith
Funktion, dieResult
erwartet. Die Funktionenresume
undresumeWithException
, die wir verwenden, sind Erweiterungsmethoden aus der Standardbibliothek, dieresumeWith
nutzen.
Wir könnten auch einen anderen Thread starten, der für eine bestimmte Dauer schlafen wird und nach dieser Zeit wiederaufnehmen wird.
Dies ist eine wichtige Beobachtung. Beachten Sie, dass wir eine Funktion erstellen können, die unsere Kontinuität nach einem festgelegten Zeitraum wieder aufnimmt. In einem solchen Fall wird die Kontinuität durch die Lambda-Funktion erfasst, wie im unten gezeigten Codeausschnitt dargestellt.
Ein solcher Mechanismus funktioniert zwar, aber er erzeugt unnötigerweise Threads, nur um sie nach gerade mal einer Sekunde Inaktivität wieder zu beenden. Threads sind kostspielig, also warum sollten wir sie verschwenden? Ein besserer Ansatz wäre, einen "Zeitgeber" einzurichten. In der JVM können wir dafür den ScheduledExecutorService
verwenden. Wir können ihn so konfigurieren, dass er continuation.resume(Unit)
nach einem festgelegten Zeitraum aufruft.
Das Unterbrechen für eine festgelegte Zeitspanne scheint eine nützliche Funktion zu sein. Lasst uns das in eine Funktion extrahieren. Wir werden sie delay
nennen.
Das Ausführungsprogramm verwendet weiterhin einen Prozessfaden, aber es handelt sich um einen Prozessfaden für alle Coroutinen, die die delay
Funktion verwenden. Dies ist deutlich besser als jedes Mal einen Prozessfaden zu sperren, wenn wir einige Zeit warten müssen.
Genau so wurde die delay
Funktion aus der Kotlin Coroutinen-Bibliothek früher implementiert. Die aktuelle Implementierung ist komplizierter, hauptsächlich um Tests zu unterstützen, aber die grundlegende Idee bleibt die gleiche.
Mit einem Wert fortfahren
Eine Sache, die Sie vielleicht interessiert, ist, warum wir Unit
an die resume
Funktion übergeben haben. Sie fragen sich vielleicht auch, warum wir Unit
als Typparameter für die suspendCoroutine
verwendet haben. Dass diese beiden gleich sind, ist kein Zufall. Unit
wird auch von der resume
Funktion zurückgegeben und ist der generische Typ des Continuation
Parameters.
Wenn wir suspendCoroutine
aufrufen, können wir angeben, welcher Typ in seiner Fortsetzung zurückgegeben wird. Derselbe Typ muss verwendet werden, wenn wir resume
aufrufen.
Dies passt nicht gut zum Vergleich mit einem Spiel. Mir ist kein Spiel bekannt, in dem man beim Fortsetzen eines gespeicherten Spiels etwas hinzufügen kann3 (es sei denn, man hat geschummelt und gegoogelt, wie man die nächste Herausforderung löst). Bei Coroutinen ergibt das jedoch vollkommen Sinn. Oft werden wir angehalten, weil wir auf Daten warten, wie eine Netzwerk-Antwort einer API. Dies ist ein häufiges Szenario. Dein Thread führt Business-Logik aus, bis er an einen Punkt kommt, wo er Daten benötigt. Also fordert er deine Netzwerkbibliothek auf, diese zu liefern. Ohne Coroutinen würde dieser Thread dann warten müssen. Das wäre eine enorme Verschwendung, denn Threads kosten viel Ressourcen, besonders wenn es sich um einen wichtigen Thread handelt, wie den Haupt-Thread auf Android. Mit Coroutinen wird er einfach angehalten und gibt der Bibliothek eine Fortführung mit der Anweisung "Sobald du diese Daten hast, sende sie einfach an die resume
Funktion". Dann kann der Thread andere Aufgaben übernehmen. Sobald die Daten vorliegen, wird der Thread dazu verwendet, ab dem Punkt fortzusetzen, an dem die Koroutine angehalten wurde.
Um dies in der Praxis zu sehen, schauen wir uns an, wie wir möglicherweise warten, bis wir einige Daten erhalten. Im folgenden Beispiel verwenden wir eine requestUser
Callback-Funktion, die extern implementiert ist.
suspendCoroutine
direkt aufzurufen ist nicht praktisch. Wir würden stattdessen lieber eine Unterbrechungsfunktion haben. Wir können es selbst extrahieren.
Aktuell werden Unterbrechbare Funktionen bereits von vielen beliebten Bibliotheken, wie zum Beispiel Retrofit und Room unterstützt. Das ist der Grund, warum wir selten Callback-Funktionen in Unterbrechbaren Funktionen verwenden müssen. Sollten Sie jedoch ein solches Bedürfnis haben, empfehle ich die Verwendung von suspendCancellableCoroutine
(anstatt suspendCoroutine
), was im Kapitel Abbruch erklärt wird.
Sie fragen sich vielleicht, was passiert, wenn uns die API statt Daten ein Problem liefert. Was ist, wenn der Dienst nicht funktioniert oder mit einem Fehler antwortet? In einem solchen Fall können wir keine Daten zurückgeben; stattdessen sollten wir eine Ausnahme von dem Ort auslösen, an dem die Coroutine unterbrochen wurde. Hier müssen wir mit einer Ausnahme fortsetzen.
Fortsetzung mit einer Ausnahme
Jede Funktion, die wir aufrufen, könnte einen Wert zurückgeben oder eine Ausnahme auslösen. Das Gleiche gilt für suspendCoroutine
. Wenn resume
aufgerufen wird, gibt es Daten zurück, die als Argument übergeben wurden. Wenn resumeWithException
aufgerufen wird, wird die Ausnahme, die als Argument übergeben wurde, aus dem Unterbrechungspunkt geworfen.
Dieser Mechanismus wird für verschiedene Arten von Problemen verwendet. Zum Beispiel, um Netzwerkausnahmen zu signalisieren.
Eine Koroutine unterbrechen, nicht eine Funktion
Etwas, das hier hervorgehoben werden muss, ist, dass wir eine Koroutine aussetzen, nicht eine Funktion. Suspendierbare Funktionen sind keine Coroutinen, sondern nur Funktionen, die eine Koroutine aussetzen können4. Stellen Sie sich vor, wir speichern eine Funktion in einer Variablen und versuchen, sie nach dem Funktionsaufruf fortzusetzen.
Das macht keinen Sinn. Es ist vergleichbar mit dem Anhalten eines Spiels und der Planung, es zu einem späteren Zeitpunkt im Spiel wieder aufzunehmen. resume
wird nie aufgerufen. Sie werden nur "Before" sehen und Ihr Programm wird nie beenden, es sei denn wir führen resume
auf einem anderen Thread oder einer anderen Coroutine aus. Um dies zu veranschaulichen, können wir eine andere Coroutine festlegen, die nach einer Sekunde resume
ausgeführt wird.
Zusammenfassung
Ich hoffe, jetzt haben Sie ein klares Bild davon, wie die Unterbrechung aus der Benutzersicht funktioniert. Es ist wichtig und wir werden es im Laufe des Buches sehen. Es ist zudem praktisch, da Sie nun Callback-Funktionen nehmen und sie in unterbrechende Funktionen umwandeln können. Wenn Sie wie ich sind und genau wissen wollen, wie Dinge funktionieren, fragen Sie sich wahrscheinlich immer noch, wie es umgesetzt wird. Wenn Sie neugierig auf dieses Thema sind, wird es im nächsten Kapitel behandelt. Falls Sie glauben, dass Sie es nicht wissen müssen, überspringen Sie es einfach. Es ist nicht sonderlich praktikabel, es enthüllt nur die Magie von Kotlin Coroutines.
Es ruft direkt suspendCoroutineUninterceptedOrReturn
auf, eine grundlegende Funktion, das bedeutet eine Funktion mit eingebauter Implementierung.
Diese Aussage ist wahr, aber ich muss sie klären. Sie könnten denken, dass wir hier suspendieren und sofort fortfahren. Das ist eine gute Intuition, aber die Wahrheit ist, dass es eine Optimierung gibt, die eine Unterbrechung verhindert, wenn das Fortfahren unmittelbar erfolgt.
Während einer Workshop-Diskussion stellte sich heraus, dass es ein solches Spiel gibt: In Don't Starve Together können Sie, wenn Sie fortfahren, die Spieler wechseln. Ich habe es selbst nicht gespielt, aber das klingt nach einer passenden Metapher für das Fortfahren mit einem Wert.
Eine unterbrechende main
Funktion ist ein Spezialfall. Der Kotlin Compiler startet sie in einer Coroutine.