Coroutinen :: praktische Erfahrung

In diesem Artikel werde ich darüber sprechen, wie Coroutinen funktionieren und wie sie erstellt werden. Betrachten Sie die Anwendung in sequentieller, paralleler Ausführung. Lassen Sie uns über Fehlerbehandlung, Debugging und Möglichkeiten zum Testen von Coroutine sprechen. Am Ende werde ich die Eindrücke, die nach Anwendung dieses Ansatzes verblieben sind, zusammenfassen und darüber sprechen.

Der Artikel wurde basierend auf den Materialien meines Berichts über MBLT DEV 2018 am Ende des Beitrags erstellt - ein Link zum Video.

Konsistenter Stil



Abb. 2.1

Was war der Zweck der Corutin-Entwickler? Sie wollten, dass die asynchrone Programmierung so einfach wie möglich ist. Es gibt nichts Einfacheres, als den Code „Zeile für Zeile“ mit den syntaktischen Konstruktionen der Sprache auszuführen: try-catch-finally, Schleifen, bedingte Anweisungen usw.

Betrachten wir zwei Funktionen. Jeder wird in einem eigenen Thread ausgeführt (Abb. 2.1). Die erste wird in Thread B ausgeführt und gibt einige Ergebnisdaten B zurück. Dann müssen wir dieses Ergebnis an die zweite Funktion übergeben, die dataB als Argument verwendet und bereits in Thread A ausgeführt wird. Mit Coroutine können wir unseren Code wie in Abb. 1 gezeigt schreiben. 2.1. Überlegen Sie, wie Sie dies erreichen können.

Funktionen longOpOnB, longOpOnA - die sogenannten Suspend-Funktionen , vor denen der Thread freigegeben wird und nach Abschluss seiner Arbeit wieder beschäftigt wird.

Damit diese beiden Funktionen tatsächlich in einem anderen Thread als dem aufgerufenen ausgeführt werden können, während ein „konsistenter“ Schreibstil beibehalten wird, müssen wir sie in den Kontext der Coroutine eintauchen.

Dazu werden Coroutinen mit dem sogenannten Coroutine Builder erstellt. In der Abbildung ist dies ein Start , aber es gibt andere, z. B. asynchron , runBlocking . Ich werde später darüber sprechen.

Das letzte Argument ist ein Codeblock, der im Kontext der Coroutine ausgeführt wird: Aufrufen von Suspend-Funktionen, was bedeutet, dass alle oben genannten Verhaltensweisen nur im Kontext der Coroutine oder in einer anderen Suspend-Funktion möglich sind.

Die Coroutine Builder-Methode enthält weitere Parameter, z. B. die Art des Starts, den Thread, in dem der Block ausgeführt wird, und andere.

Lebenszyklusmanagement


Coroutine Builder gibt uns den Rückgabewert als Rückgabewert - eine Unterklasse der Jobklasse (Abb.2.2). Damit können wir den Lebenszyklus von Corutin steuern.

Beginnen Sie mit der start () -Methode, brechen Sie mit der cancel () -Methode ab, warten Sie, bis der Job mit der join ( ) -Methode abgeschlossen ist, abonnieren Sie das Jobabschlussereignis und vieles mehr.


Abb. 2.2

Durchflussänderung


Sie können den Ablauf der Coroutine-Ausführung ändern, indem Sie das Kontextelement der Coroutine ändern, die für die Planung verantwortlich ist. (Abb. 2.3)

Beispielsweise wird Corutin 1 in einem UI- Thread ausgeführt, während Corutin 2 in einem Thread aus dem Dispatchers.IO- Pool entnommen wird.


Abb.2.3

Die Coroutine-Bibliothek bietet auch eine Suspend-Funktion mit Context (CoroutineContext) , mit der Sie im Kontext einer Coroutine zwischen Threads wechseln können. Das Wechseln zwischen Threads kann daher recht einfach sein:


Abb. 2.4.

Wir starten unsere Coroutine auf UI-Thread 1 → zeigen die Lastanzeige → wechseln zu Arbeitsthread 2, geben den Hauptthread frei → wir führen dort eine lange Operation durch, die im UI-Thread nicht ausgeführt werden kann → geben das Ergebnis zurück zu UI-Thread 3 → und arbeiten dort bereits Damit werden die empfangenen Daten gerendert und die Ladeanzeige ausgeblendet.

Bis jetzt sieht es ziemlich bequem aus, mach weiter.

Suspend-Funktion


Betrachten Sie die Arbeit von Corutin am Beispiel des häufigsten Falls - Arbeiten mit Netzwerkanforderungen mithilfe der Retrofit 2-Bibliothek.

Als erstes müssen wir den Rückrufaufruf in eine Suspend- Funktion konvertieren, um die Coroutine-Funktion nutzen zu können:


Abb. 2.5

Um den Status der Coroutine zu steuern, bietet die Bibliothek Funktionen der Form suspendXXXXCoroutine , die ein Argument bereitstellen, das die Continuation- Schnittstelle implementiert. Dabei werden die Methoden resumeWithException und resume verwendet, mit denen die Coroutine im Falle eines Fehlers bzw. Erfolgs fortgesetzt werden kann.

Als Nächstes werden wir herausfinden, was passiert, wenn die Methode resumeWithException aufgerufen wird, und zunächst sicherstellen, dass der Netzwerkanforderungsaufruf irgendwie abgebrochen werden muss.

Funktion aussetzen. Anrufstornierung


Um den Aufruf und andere Aktionen im Zusammenhang mit der Freigabe nicht verwendeter Ressourcen abzubrechen, können Sie bei der Implementierung der Suspend-Funktion die sofort einsatzbereite suspendCancellableCoroutine- Methode verwenden (Abb. 2.6). Hier implementiert das Blockargument bereits die CancellableContinuation- Schnittstelle, mit einer der zusätzlichen Methoden - invokeOnCancellation - können Sie sich für einen Fehler oder ein erfolgreiches Coroutine- Abbruchereignis anmelden . Daher ist es hier auch notwendig, den Methodenaufruf abzubrechen.


Abb. 2.6

Änderungen in der Benutzeroberfläche anzeigen


Nachdem die Suspend-Funktion für Netzwerkanforderungen vorbereitet wurde, können Sie ihren Aufruf im Coroutine-UI-Stream als sequentiell verwenden, während der Stream während der Ausführung der Anforderung frei ist und der Nachrüst-Stream für die Anforderung verwendet wird.

Daher implementieren wir das Verhalten asynchron in Bezug auf den UI-Stream, schreiben es jedoch in einem konsistenten Stil (Abb. 2.6).

Wenn Sie nach Erhalt der Antwort die harte Arbeit erledigen müssen, z. B. die empfangenen Daten in die Datenbank schreiben, kann diese Funktion, wie bereits gezeigt, problemlos mit withContext im Pool der Backstream -Flows ausgeführt und die Ausführung auf der Benutzeroberfläche ohne eine einzige Codezeile fortgesetzt werden.


Abb. 2.7

Leider ist dies nicht alles, was wir für die Anwendungsentwicklung benötigen. Betrachten Sie die Fehlerbehandlung.

Fehlerbehandlung: try-catch-finally. Coroutine abbrechen: CancellationException


Eine Ausnahme, die nicht in der Coroutine abgefangen wurde, gilt als nicht behandelt und kann zum Absturz der Anwendung führen. Zusätzlich zu normalen Situationen wird eine Ausnahme ausgelöst, indem die Coroutine mit der Methode resumeWithException in der entsprechenden Zeile des Aufrufs der Suspend-Funktion fortgesetzt wird. In diesem Fall wird die als Argument übergebene Ausnahme unverändert ausgelöst. (Abb. 2.8)


Abb. 2.8

Für die Ausnahmebehandlung steht das Standard-Sprachkonstrukt try catch finally zur Verfügung. Der Code, der den Fehler in der Benutzeroberfläche anzeigen kann, hat nun die folgende Form:


Abb. 2.9

Beim Abbrechen der Coroutine, die durch Aufrufen der Job # cancel-Methode erreicht werden kann, wird eine CancellationException ausgelöst. Diese Ausnahme wird standardmäßig behandelt und führt nicht zu Abstürzen oder anderen negativen Folgen.

Wenn Sie jedoch das try / catch- Konstrukt verwenden, wird es im catch-Block abgefangen, und Sie müssen damit rechnen, wenn Sie nur wirklich „fehlerhafte“ Situationen behandeln möchten. Beispielsweise wird eine Fehlerbehandlung in der Benutzeroberfläche bereitgestellt, wenn Anforderungen abgebrochen oder Fehler protokolliert werden können. Im ersten Fall wird der Fehler dem Benutzer angezeigt, obwohl er tatsächlich nicht vorhanden ist, und im zweiten Fall wird eine nutzlose Ausnahme protokolliert und die Berichte werden unübersichtlich.

Um die Situation beim Abbrechen von Coroutinen zu ignorieren, müssen Sie den Code leicht ändern:


Abb. 2.10

Fehlerprotokollierung


Betrachten Sie den Exception Exception Stack Trace.

Wenn Sie eine Ausnahme direkt in den Coroutine-Codeblock auslösen (Abb. 2.11), sieht der Stack-Trace ordentlich aus. Mit nur wenigen Aufrufen von Coroutine werden die Zeile und die Informationen zur Ausnahme korrekt angezeigt. In diesem Fall können Sie anhand der Stapelverfolgung leicht erkennen, wo genau, in welcher Klasse und in welcher Funktion die Ausnahme ausgelöst wurde.


Abb. 2.11

Ausnahmen, die an die resumeWithException- Methode von suspend- Funktionen übergeben werden, enthalten jedoch in der Regel keine Informationen über die Coroutine, in der sie aufgetreten ist. Wenn Sie beispielsweise (Abb. 2.12) die Coroutine der zuvor implementierten Suspend-Funktion mit derselben Ausnahme wie im vorherigen Beispiel fortsetzen, gibt die Stapelverfolgung keine Informationen darüber, wo speziell nach dem Fehler gesucht werden muss.


Abb. 2.12

Um zu verstehen, welche Coroutine mit einer Ausnahme wieder aufgenommen wurde, können Sie das Kontextelement CoroutineName verwenden . (Abb. 2.13)

Das CoroutineName- Element wird zum Debuggen verwendet, indem der Name der Coroutine übergeben wird. Sie können es in Suspend-Funktionen extrahieren und beispielsweise die Ausnahmemeldung ergänzen. Das heißt, zumindest wird klar sein, wo nach einem Fehler gesucht werden muss.

Dieser Ansatz funktioniert nur, wenn die Suspend-Funktion davon ausgeschlossen ist:


Abb. 2.13

Fehlerprotokollierung. ExceptionHandler


Um die Ausnahmeprotokollierung für eine bestimmte Coroutine zu ändern, können Sie Ihren eigenen ExceptionHandler festlegen, der eines der Elemente des Coroutine-Kontexts ist. (Abb. 2.14)

Der Handler muss die CoroutineExceptionHandler- Schnittstelle implementieren. Mit dem überschriebenen Operator + für den Coroutine-Kontext können Sie den Standard-Ausnahmebehandler durch Ihren eigenen ersetzen. Die nicht behandelte Ausnahme fällt in die handleException- Methode, mit der Sie alles tun können, was Sie brauchen. Zum Beispiel völlig ignorieren. Dies geschieht, wenn Sie den Handler leer lassen oder Ihre eigenen Informationen hinzufügen:


Abb. 2.14

Mal sehen, wie die Protokollierung unserer Ausnahme aussehen könnte:

  1. Sie müssen sich an die CancellationException erinnern, die wir ignorieren möchten.
  2. Fügen Sie Ihre eigenen Protokolle hinzu.
  3. Denken Sie an das Standardverhalten, zu dem das Protokollieren und Beenden der Anwendung gehört. Andernfalls wird die Ausnahme einfach „ausgeblendet“ und es ist nicht klar, was passiert ist.

Für den Fall, dass eine Ausnahme ausgelöst wird, wird eine Stapelverfolgungsliste mit den hinzugefügten Informationen an den Logcat gesendet:


Abb. 2.15

Parallele Ausführung. asynchron


Betrachten Sie den Parallelbetrieb von Suspend-Funktionen.

Async eignet sich am besten zum Organisieren paralleler Ergebnisse aus mehreren Funktionen. Async, wie Launch - Coroutine Builder. Die Bequemlichkeit besteht darin, dass mit der Methode await () bei Erfolg Daten zurückgegeben werden oder eine Ausnahme ausgelöst wird, die während der Ausführung der Coroutine aufgetreten ist. Die Methode wait wartet auf den Abschluss der Coroutine, falls diese noch nicht abgeschlossen ist. Andernfalls wird das Ergebnis der Arbeit sofort zurückgegeben. Beachten Sie, dass das Warten eine Suspend-Funktion ist und daher nicht außerhalb des Kontexts einer Coroutine oder einer anderen Suspend-Funktion ausgeführt werden kann.

Wenn Sie asynchron arbeiten und Daten von zwei Funktionen parallel abrufen, sieht dies ungefähr so ​​aus:


Abb. 2.16

Stellen Sie sich vor, wir stehen vor der Aufgabe, Daten von zwei Funktionen parallel abzurufen. Dann müssen Sie sie kombinieren und anzeigen. Im Fehlerfall muss die Benutzeroberfläche gezeichnet werden, um alle aktuellen Anforderungen abzubrechen. Ein solcher Fall ist in der Praxis häufig anzutreffen.

In diesem Fall muss der Fehler wie folgt behandelt werden:

  1. Bringen Sie die Fehlerbehandlung in jedes Async-Corutin.
  2. Brechen Sie im Fehlerfall alle Coroutinen ab. Glücklicherweise ist es dafür möglich, einen übergeordneten Job anzugeben, bei dessen Stornierung alle untergeordneten Jobs storniert werden.
  3. Wir haben eine zusätzliche Implementierung entwickelt, um zu verstehen, ob alle Daten erfolgreich geladen wurden. Wir gehen beispielsweise davon aus, dass beim Empfang von Daten ein Fehler aufgetreten ist, wenn wait null zurückgegeben hat.

Vor diesem Hintergrund wird die Implementierung der elterlichen Coroutine etwas komplizierter. Die Implementierung von Async-Corutin ist ebenfalls kompliziert:


Abb. 2.17

Dieser Ansatz ist nicht der einzig mögliche. Beispielsweise können Sie die parallele Ausführung mit Fehlerbehandlung mithilfe von ExceptionHandler oder SupervisorJob implementieren.

Verschachtelte Coroutinen


Schauen wir uns die Arbeit der verschachtelten Coroutine an.

Standardmäßig wird eine verschachtelte Coroutine mithilfe eines externen Bereichs erstellt und erbt ihren Kontext. Infolgedessen wird die verschachtelte Coroutine eine Tochter und der externe Elternteil.

Wenn wir die externe Coroutine abbrechen, werden auch die auf diese Weise erstellten verschachtelten Coroutinen, die im vorherigen Beispiel verwendet wurden, abgebrochen. Dies ist auch nützlich, wenn Sie den Bildschirm verlassen, wenn Sie aktuelle Anforderungen abbrechen müssen. Darüber hinaus wartet das Eltern-Corutin immer auf die Fertigstellung der Tochter.

Sie können eine von der externen unabhängige Coroutine mithilfe eines globalen Bereichs erstellen. In diesem Fall funktioniert die verschachtelte Coroutine, wenn die externe Coroutine abgebrochen wird, so weiter, als wäre nichts passiert:


Abb. 2.18

Sie können der global verschachtelten Coroutine ein untergeordnetes Element erstellen, indem Sie das Kontextelement durch den Jobschlüssel durch den übergeordneten Job ersetzen, oder Sie können den Kontext der übergeordneten Coroutine vollständig verwenden. In diesem Fall ist jedoch zu beachten, dass alle Elemente der übergeordneten Coroutine übernommen werden: der Thread-Pool, der Ausnahmebehandler usw.


Abb. 2.19

Wenn Sie Coroutine von außen verwenden, müssen Sie ihnen die Möglichkeit geben, entweder eine Instanz des Jobs oder den Kontext des übergeordneten Elements zu installieren. Und Bibliotheksentwickler müssen die Möglichkeit in Betracht ziehen, es als Kind zu installieren, was zu Unannehmlichkeiten führt.

Haltepunkte


Coroutinen wirken sich auf die Anzeige von Objektwerten im Debug-Modus aus. Wenn Sie einen Haltepunkt in die nächste Coroutine der logData- Funktion einfügen , sehen wir beim Auslösen , dass hier alles in Ordnung ist und die Werte korrekt angezeigt werden:


Abb. 2.20

Holen Sie sich jetzt dataA mithilfe der verschachtelten Coroutine und lassen Sie einen Haltepunkt in logData :


Abb. 2.21

Der Versuch, diesen Block zu erweitern, um die gewünschten Werte zu finden, schlägt fehl. Daher wird das Debuggen in Gegenwart von Suspend-Funktionen schwierig.

Unit Testing


Unit-Tests sind ziemlich einfach. Sie können hierfür den Coroutine Builder runBlocking verwenden . runBlocking blockiert einen Thread, bis alle verschachtelten Coroutinen fertig sind. Genau das benötigen Sie zum Testen.

Wenn beispielsweise bekannt ist, dass irgendwo innerhalb der Methode Coroutine verwendet wird, um sie zu implementieren, müssen Sie sie zum Testen der Methode nur in runBlocking einschließen .

Mit runBlocking kann eine Suspend-Funktion getestet werden:


Abb. 2.22

Beispiele


Abschließend möchte ich einige Beispiele für die Verwendung von Corutin zeigen.

Stellen Sie sich vor, wir müssen drei Abfragen A, B und C parallel ausführen, ihre Fertigstellung anzeigen und den Zeitpunkt der Fertigstellung der Anforderungen A und B widerspiegeln.

Dazu können Sie einfach die Abfragekoroutinen A und B in eine gemeinsame zusammenfassen und wie mit einem einzigen Ganzen damit arbeiten:


Abb. 2.23

Das folgende Beispiel zeigt, wie Sie mit der regulären for-Schleife periodische Abfragen mit einem Intervall von 5 Sekunden ausführen:


Abb. 2.24

Schlussfolgerungen


Von den Minuspunkten stelle ich fest, dass Coroutinen ein relativ junges Werkzeug sind. Wenn Sie sie also für das Produkt verwenden möchten, sollten Sie dies mit Vorsicht tun. Es gibt Schwierigkeiten beim Debuggen, ein kleines Boilerplate bei der Implementierung offensichtlicher Dinge.

Im Allgemeinen sind Coroutinen recht einfach zu verwenden, insbesondere für die Implementierung nicht komplizierter asynchroner Aufgaben. Insbesondere aufgrund der Tatsache, dass Standard-Sprachkonstrukte verwendet werden können. Coroutinen sind für Unit-Tests leicht zugänglich, und all dies wird sofort von derselben Firma geliefert, die die Sprache entwickelt hat.

Video melden


Es stellte sich heraus, viele Briefe. Für diejenigen, die mehr hören möchten - Video aus meinem Bericht über MBLT DEV 2018 :


Nützliche Materialien zum Thema:


Source: https://habr.com/ru/post/de442920/


All Articles