© Alvaro Cabrera Jimenez/Shutterstock.com
Teil 3: Beschränkte Nebenläufigkeit, deterministisches Verhalten

Koroutinen in Kotlin: Structured Concurrency


Koroutinen implementieren das Konzept Structured Concurrency. Dieses Konzept beschränkt bewusst die Stellen, an denen es möglich ist, nebenläufige Aktivitäten zu starten. Im Gegenzug wird die Verständlichkeit und Nachvollziehbarkeit von nebenläufigen Koroutinen verbessert. Zusätzlich wird die Fehlerbehandlung deterministisch und es wird sichergestellt, dass keine Zombiekoroutinen entstehen.

In den ersten beiden Artikeln über Koroutinen [1] wurden die Grundlagen für sequenzielle und nebenläufige Koroutinen an verschiedenen praktischen Beispielen gezeigt. Dieser Teil fokussiert sich auf die Mechanismen und Konzepte, die hinter den Koroutinen stecken. Insbesondere das Thema Fehlerbehandlung, das bisher ignoriert wurde, soll dieses Mal behandelt werden.

Der vollständige Quellcode zu diesem Artikel ist unter [2] zu finden.

Ein bisschen Informatikgeschichte zu „goto“

Die meisten Leser werden irgendwann einmal von dem Beitrag „Go To Statement Considered Harmful“ von Edsger Dijkstra [3] gehört haben. Diese Arbeit ist eine der Grundlagen der strukturierten Programmierung und somit die Basis der meisten aktuellen Programmiersprachen. Für diejenigen, die das klassische goto nicht mehr kennen, kommt hier eine kleine Auffrischung des Konzepts und der Probleme.

In einigen Programmiersprachen ist es mit goto möglich, beliebig zwischen Programmteilen zu switchen. Man kann also mitten aus einer Funktion in eine andere Funktion springen. Als Veranschaulichung soll ein kleines C-Programm dienen, siehe Listing 1.

Listing 1

jmp_buf go; void helper() { printf("Zwei, "); // Springe zum „If“ in der Main-Funktion und liefere dort 1 zurück longjmp(go,1); } int main(int argc, char *argv[]) { printf("Eins, "); // Bereitet den Sprung vor und liefert 0 beim initialen Aufruf zurück.  // Nach dem longjmp wird dagegen 1 geliefert. Deswegen wird das if  // nur einmal beim ersten Aufruf durchlaufen. if (!setjmp(go)) { helper();  // Wird nicht erreicht werden printf("Drei, "); } printf("Vier \n"); }

Die C-Syntax in diesem Beispiel ist Java sehr ähnlich und sollte kein Problem darstellen. Das Entscheidende an diesem Code sind die beiden Funktionen setjmp und longjmp. Die erste Funktion merkt sich eine Programmstelle inklusive Aufrufstack in einer Variable (go). Zu dieser Stelle wird in der helper()-Funktion mit longjmp gesprungen. Das führt dazu, dass der Aufruf der helper()-Funktion nicht wie erwartet zurückkommt und „Drei“ ausgegeben wird, sondern dass die helper()-Funktion zu dem if-Statement in der main()-Funktion springt. Da diese Sprünge prinzipiell jederzeit passieren können und die Aufrufhierarchie von Funktionen völlig ignoriert wird, kann man durch das Lesen einzelner Programmstellen den Ablauf nicht mehr vorhersagen.

Bei der Einführung der strukturierten Programmierung wurde diese Art von nicht lokalen Sprüngen bewusst verboten. Dadurch ist es möglich, den Ablauf von Funktionen als Blackbox zu betrachten und immer vorherzusagen, dass Funktionen an die Aufrufstelle zurückkehren (die Einführung von Exceptions hat dieses einfache Konzept wieder etwas verkompliziert).

Unstrukturierte Nebenläufigkeit

Vor etwa einem Jahr erschien der Artikel „Notes on structured concurrency, or: Go statement considered harmful“ von Nathaniel J. Smith [4], der die Probleme von goto mit dem unstrukturierten Starten von nebenläufigen Aktivitäten verglichen hat. Dabei ist es egal, ob wir von einem Thread-Modell oder einem Koroutinenmodell ausgehen. Das Starten einer nebenläufigen Aktivität wurde in dem Artikel allgemein mit dem Begriff „go“ bezeichnet. Das Problem, das durch unkontrolliertes Starten von parallelen Aktivitäten entsteht, lässt sich gut in Listing 2 erkennen.

Listing 2

fun createCollage() { val image: BufferedImage = ... val file = File("dogs.png") saveImage(image, file) sendEMailWithImage(file) } fun saveImage(image: BufferedImage, file: File) {  // startet einen neuen Thread thread { // entspricht go ImageIO.write(image, "png", file) } }

Die Funktion createCollage ruft die Funktion saveImage auf. Anschließend soll das gespeicherte Bild per Mail verschickt werden: sendEMailWithImage. Würde man nur die Funktion createCollage betrachten, könnte man ziemlich sicher sein, dass der Ablauf so funktioniert. Doch ein Blick in die Funktion saveImage offenbart, dass zum Schreiben des Bilds ein neuer Thread gestartet wird und es nicht garantiert ist, dass dieser Thread nach dem Beenden der Funktion saveImage schon beendet ist.

Das Problem ist, dass es überall im Code möglich ist, neue Threads zu starten, und es keine Klammer gibt, die auf das Beenden dieser neuen Threads wartet oder auf Fehler reagiert. Dadurch wird es unmöglich, Nebenläufigkeit innerhalb von Funktionen als Blackbox zu betrachten, ähnlich wie beim goto-Befehl, bei dem das Blackboxverhalten für sequenzielle Aufrufe nicht gegeben ist.

Strukturierte Nebenläufigkeit

Die Lösung für dieses Problem ist ähnlich der Lösung des goto-Problems. Man verzichtet bewusst auf den Freiheitsgrad, überall neue Aktivitäten zu starten. Dafür erhält man wieder das Blackboxverhalten und es wird möglich, das Zusammenspiel von Nebenläufigkeit durch das Betrachten lokaler Aufrufe nachzuvollziehen. Nachfolgend wird das Arbeiten mit und die Konsequenzen von Structured Concurrency am konkreten Beispiel von Koroutinen gezeigt.

In den ersten Versionen war es möglich, die Funktionen launch und async an beliebigen Stellen im Code aufzurufen und damit neue Koroutinen zu starten. Erst mit der Version 0.26 wurde strukturierte Nebenläufigkeit eingeführt und jede Koroutine muss nun über einen CoroutineScope angelegt werden. Technisch wurde das dadurch erreicht, dass die ehemals globalen Funktionen in das Interface CoroutineScope verschoben worden. Das Ziel dabei ist, dass neue Koroutinen immer einen Parent zugewiesen bekommen und somit eine Hierarchie bilden. Schauen wir uns Listing 3 an, um die einzelnen Schritte zu diskutieren.

Listing 3

val scope = CoroutineScope(Dispatchers.Default) scope.launch { // A - this is CoroutineScope launch { /* A1 */ } } scope.launch { // B - this is CoroutineScope launchTwoChildren() } suspend fun launchTwoChildren(): String { // launch geht hier nicht val result = coroutineScope { // this is C...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang