Beliebte Suchanfragen

Cloud Native

DevOps

IT-Security

Agile Methoden

Java

//

Einführung in Kotlin Coroutines

15.5.2018 | 4 Minuten Lesezeit

Es gibt kaum noch Anwendungen, die ohne Nebenläufigkeit auskommen. In Java löst man Probleme mit Asynchronität und Nebenläufigkeit in der Regel mit Threads. Leider bringen Threads einen relativ großen Overhead mit sich, dazu kommen Callbacks (oder noch schlimmer: verschachtelte Callbacks), die es schwerer machen, den Code zu lesen und zu verstehen.
Kotlin bietet als Lösungsansatz sogenannte Coroutines an, mit denen man nebenläufige Programme ganz natürlich – das heißt sequenziell und ohne Callbacks – schreiben kann.

Setup

Um Coroutines nutzen zu können, muss man Kotlin in der Version 1.3 verwenden und zusätzlich den coroutines-core zu den Dependencies hinzufügen:


dependencies {
    ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1'
}
...

Das war’s schon und es kann losgehen!

Sequenziell und trotzdem nebenläufig

Coroutines ermöglichen nebenläufige Programmierung im sequenziellen Stil. Ganz ohne Callbacks, Promises oder Threads.
Anstatt einen Thread zu starten und über ein Callback auf das Ergebnis dessen Berechnung zu warten, kann ich in der nächsten Zeile mit diesem Ergebnis einfach weiterarbeiten.

In Code gegossen sieht das dann so aus:


suspend fun showMails() {
   val emails = retrieveMails() 

   emails.forEach {
       println(it)
   }
}

suspend fun retrieveMails(): List {
   ... //get mails from the internet
   return ...
   }
}

In diesem Snippet sind zwei Dinge interessant:
Das Keyword suspend kennzeichnet eine suspending function. Suspend ist ein Hinweis darauf, dass diese Funktion asynchron ist und nicht sofort ein Ergebnis liefern muss.
Die Funktion retrieveMails() stellt hier eine potenziell blockierende Operation, also zum Beispiel einen Network call, dar. Ohne das suspend Keyword würde diese Funktion den aufrufenden Thread so lange blockieren, bis retrieveMails() (und das anschließende Ausgeben der Mails) abgeschlossen ist. Man müsste sie also z. B. in einem eigenen Thread ausführen.

Suspending functions können jedoch nicht aus normalen Funktionen aufgerufen werden, sondern müssen über einen sogenannten Coroutine Builder gestartet werden.


fun main(args: Array) {
   launch { showMails() }
}

Coroutine Builder

Die Funktion launch{} ist ein solcher Coroutine Builder. Startet man jetzt die Anwendung, würde man die erwartete Ausgabe aus showMails() jedoch nicht sehen. Der Aufruf von launch{} startet die suspending function, interessiert sich jedoch nicht für das Ergebnis. Coroutines verhalten sich wie Daemon-Threads. Sie halten den Prozess, aus dem sie gestartet wurden, nicht am Leben. Die main-Funktion würde also terminieren, bevor alle Mails abgeholt worden sind.
Um das zu verhindern, muss ich irgendwo auf das Ergebnis warten. Das kann ich z. B. mit join() und dem Coroutine Builder runBlocking{} erreichen.


fun main(args: Array)  {
   val job = launch { showMails() }
   println("You've got mail:")

   runBlocking {
       job.join()
   }
}

Die join()-Funktion wartet darauf, dass der Job beendet wird. Sie ist selbst eine suspending function und blockiert daher auch nicht den aktuellen Thread. Damit das funktioniert, muss sie über den Coroutine Builder runBlocking{} ausgeführt werden.
Um die Verbindung zwischen der asynchronen und synchronen Welt herzustellen, bietet es sich daher an, die main-Funktion als Einstiegspunkt in die gesamte Anwendung selbst als blocking zu definieren:


fun main(args: Array) = runBlocking {
   val job = launch { showMails() }
   println("You've got mail:")
  
   job.join()
}

Neben launch{} und runBlocking{} gibt es noch weitere Coroutine Builder.
Der async{} Builder lässt mich Coroutines auf ähnliche Art benutzen wie async/await in JavaScript oder C#.


fun main(args: Array) = runBlocking {
   val numbers = async {
       List(10) { i ->
           // create a list with 10 elements, waiting 1000 millis in every iteration
           delay(1000)
           i
       }
   }

   println("Your numbers:")

   numbers.await().forEach {
       println(it)
   }
}

Der Coroutine Builder async{} gibt ein Objekt des Typs Deferred zurück. Auf diesem Objekt kann dann mittels await() auf das Ergebnis des async-Blocks (also die Liste mit 10 Ints) gewartet werden. Auch diese Operation blockiert den Thread.

Coroutines als Alternative zu Threads

Aber was soll das? Wo ist der Unterschied zu Threads?

Coroutines sind viel leichtgewichtiger als Threads. Das bedeutet, dass ich 100000 Coroutines gleichzeitig starten kann, ohne in einen OutOfMemoryError zu laufen:


fun main(args: Array) = runBlocking {
    val jobs = List(100_000) { i -> 
        launch {
            delay(1000L)
            println(i)
        }
    }
    jobs.forEach { it.join() } // wait for all jobs to complete
}

Um dasselbe mit Threads zu machen, muss ich lediglich den Coroutine Builder launch{} durch Kotlins thread{}-Funktion austauschen und das delay() durch ein Thread.sleep() ersetzen:


 fun main(args: Array) = runBlocking {
   val jobs = List(100_000) { i->
       thread {
           sleep(1000L)
           println(i)
       }
   }
   jobs.forEach { it.join() } // wait for all jobs to complete
}

Diese Variante führt fast sicher zu einem OutOfMemoryError (“unable to create new native thread”), da jeder Thread (je nach Plattform) zwischen 256KB und 1024KB benötigt.

Coroutines sind also wie leichtgewichtige Daemon-Threads und werden über sogenannte Coroutine Builder gestartet.

Continuation-passing style

Im Grunde funktionieren Coroutines über Callbacks. Jede mit suspend definierte Funktion erhält einen unsichtbaren Parameter des Typs Continuation.
Diese Art der Kontrollflusssteuerung wird daher auch Continuation-passing style genannt.

Am Beispiel vom Anfang des Artikels kann man sich das in etwa so vorstellen:


fun showMails() {
   val emails = retrieveMails { emails ->

      emails.forEach {
          println(it)
      }
  }
}

fun retrieveMails(callback: Continuation>) {
   val emails = …  
   callback(emails)
   }
}

Das Komfortable daran ist aber, dass diese Callbacks vom Compiler generiert werden.

Mit nur einem neuen Keyword, suspend, und einigen Compiler-Tricks ermöglichen es Kotlins Coroutines, asynchronen Code zu schreiben, ohne dabei vom natürlichen sequenziellen Programmierstil abzuweichen.

Beitrag teilen

Gefällt mir

0

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen.

Wir helfen deinem Unternehmen.

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.