Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Kotlin Coroutine Deep Dive

Kotlin Coroutine Deep Dive

Kotlin gilt als moderne Alternative zu Java. Die schlanke Syntax und sinnvolle Erweiterungen führen zu gut lesbarem Code. Ein Bereich, in dem sich Kotlin besonders profiliert, sind Koroutinen. Wo Nebenläufigkeit gefordert ist, bieten sie sich als leichtgewichtige Alternative an. Aber wie verwendet man Koroutinen richtig? Worauf sollte man achten, damit die Anwendung nicht doch zu ressourcenhungrig wird? Dieser Talk für Entwickler zeigt anhand vieler Beispiele, wie Koroutinen verwendet werden und was man mit ihnen alles anstellen kann.

Thomas Künneth

February 05, 2020
Tweet

More Decks by Thomas Künneth

Other Decks in Programming

Transcript

  1. Nebenläufigkeit Unabhängig voneinander rechnen Gemeinsam ein Ergebnis ermitteln Mehrere Dinge

    gleichzeitig tun Auf Ergebnis eines (entfernten) Prozeduraufrufs warten und trotzdem mit anderen Programmteilen weiter machen Asynchrone Programmierung
  2. Auf das Ergebnis eines (entfernten) Prozeduraufrufs warten und trotzdem mit

    anderen Programmteilen weiter machen ist Gegenstand dieses Talks
  3. Blockierende Oberflächen § Praktisch alle relevanten UI-Frameworks sind single-threaded §

    Operationen > 1000 ms fallen dem Nutzer auf § Deshalb: § Nur Aktualisieren der Oberfläche auf dem UI-Thread § Langläufer woanders ausführen
  4. java_examples/BadButtonDemo.java private void good(JLabel result) { new Thread(() -> {

    int i; do { i = (int) (Math.random() * Integer.MAX_VALUE); System.out.println(i); } while (i != Integer.MAX_VALUE / 3); final int ii = i; SwingUtilities.invokeLater(() -> { result.setText(Integer.toString(ii)); }); }).start(); }
  5. § JavaScript: async in Verbindung mit Promise § C#: async

    und await in Verbindung mit Task § Kotlin: Koroutinen
  6. ButtonDemo.kt fun good(result: JLabel) { GlobalScope.launch { var i: Int

    do { i = (Math.random() * Int.MAX_VALUE).toInt() println(i) } while (i != Int.MAX_VALUE / 3) SwingUtilities.invokeLater { result.text = i.toString() } } }
  7. https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#terminology A coroutine is an instance of [a] suspendable computation.

    It is conceptually similar to a thread, in the sense that it takes a block of code to run and has a similar life-cycle — it is created and started, but it is not bound to any particular thread. It may suspend its execution in one thread and resume in another one. Moreover, like a future or promise, it may complete with some result (which is either a value or an exception).
  8. TL;DR; § Koroutinen können unterbrechen und wieder fortgesetzt werden §

    Sind wie leichtgewichtige Threads § Kotlin nutzt für die Ausführung irgendwie Threads
  9. SimpleCoroutineDemo.kt import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch fun main() {

    print("Hello, ") GlobalScope.launch { delay(1000L) println("World!") } Thread.sleep(2000L) } • delay() pausiert Koroutine, hält aktuellen Thread aber nicht an • Mischung von blockierenden und nicht blockierenden Aufrufen • Unschön: Wir raten die Ausführdauer
  10. CoroutineScope § Verwaltet und steuert eine oder mehrere Koroutinen §

    Kann Koroutinen starten und abbrechen § Wird bei Abbrüchen und Fehlern informiert § Legt deren Gültigkeit fest § GlobalScope-Koroutinen sind nur bzgl. der Laufzeit der Anwendung eingeschränkt § Plattformen und Apps können eigene Scopes definieren
  11. CoroutineContext § Jede Koroutine wird in einem bestimmten Kontext ausgeführt

    (CoroutineContext) § Wird über CoroutineScope.coroutineContext zur Verfügung gestellt § Als Index-basierte Menge von Elementen implementiert (eine Mischung aus Set und Map) § Werte können mit + hinzugefügt werden
  12. § Eine neue Koroutine erbt den Elternkontext § Elternkontext =

    Defaults + geerbter Kontext + Argumente (z. B. aus launch) § neuer CoroutineContext = Elternkontext + Job()
  13. Wichtige Elemente sind... § CoroutineDispatcher § Job § CoroutineExceptionHandler §

    CoroutineName Kann launch übergeben werden: CoroutineName("Hallo")
  14. Coroutine Dispatcher § Legen den/die Threads für die Ausführung von

    Koroutinen fest § Können Koroutinen auf einen Thread beschränken, in einem Pool ablaufen lassen, oder ohne Einschränkungen
  15. § Dispatchers.Default Ausführung in einem Thread-Pool, der sich an der

    Zahl der Kerne orientiert § Dispatchers.Main Ausführung nur auf dem Main Thread § Dispatchers.IO Für lang laufende/blockierende I/O-Operationen § Dispatchers.Unconfined Ausführung auf beliebigen Thread möglich (sollte nicht verwendet werden)
  16. https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#terminology A coroutine builder [is] a function that takes some

    suspending lambda as an argument, creates a coroutine, and, optionally, gives access to its result in some form. For example, launch{}, future{}, and sequence{} [..] are coroutine builders. The standard library provides primitive coroutine builders that are used to define all other coroutine builders.
  17. A suspending lambda [is a] a block of code that

    have to run in a coroutine. It looks exactly like an ordinary lambda expression but its functional type is marked with suspend modifier. Just like a regular lambda expression is a short syntactic form for an anonymous local function, a suspending lambda is a short syntactic form for an anonymous suspending function. It may suspend execution of the code without blocking the current thread of execution by invoking suspending functions. For example, blocks of code in curly braces following launch, future, and sequence functions [...] are suspending lambdas. https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#terminology
  18. Coroutine Builder: TL;DR; § Starten Koroutinen § Beispiele: launch, async,

    ... § Erweitern CoroutineScope § Können Parameter erhalten § Start Modus § Kontexterweiterungen
  19. Start Modi § DEFAULT Sofort mit Ausführung beginnen § ATOMIC

    ähnlich DEFAULT; kann erst nach Beginn der Ausführung abgebrochen werden § LAZY Start nur bei Bedarf (z. B. Zugriff auf Ergebnis) § UNDISPATCHED Sofortige Ausführung auf aktuellen Thread, aber Unterbrechung sobald suspension point erreicht wird
  20. java_examples/BlockingDemo.java public class BlockingDemo { public static void main(String[] args)

    { Thread t = new Thread(() -> { try { Thread.sleep(1000); System.out.println("World!"); } catch (InterruptedException e) { /**/ } }); System.out.print("Hello, "); t.start(); try { t.join(); } catch (InterruptedException e) { /**/ } } }
  21. A suspending function is a function that is marked with

    suspend modifier. It may suspend execution of the code without blocking the current thread of execution by invoking other suspending functions. A suspending function cannot be invoked from a regular code, but only from other suspending functions and from suspending lambdas [...]. For example, await() and yield() [...] are suspending functions that may be defined in a library. The standard library provides primitive suspending functions that are used to define all other suspending functions. https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#terminology
  22. TL;DR; § suspend functions sind Grundbausteine von Koroutinen § Machen

    irgendwann eine Pause oder müssen auf ein Ergebnis warten § Blockieren aber nicht den aktuellen Thread § Werden aus Koroutinen oder anderen suspending functions aufgerufen § Liefern wie normale Funktionen ein Ergebnis
  23. https://github.com/Kotlin/KEEP/blob/master/proposals/coroutines.md#terminology A suspension point is a point during coroutine execution

    where the execution of the coroutine may be suspended. Syntactically, a suspension point is an invocation of suspending function, but the actual suspension happens when the suspending function invokes the standard library primitive to suspend the execution. A continuation is a state of the suspended coroutine at suspension point. It conceptually represents the rest of its execution after the suspension point. Unter Umständen wird eine suspend function auf einem anderen Thread fortgesetzt als den, auf dem sie unterbrochen wurde. Dieses Verhalten wird durch Coroutine Dispatcher gesteuert.
  24. RunBlockingJoinDemo.kt import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun

    main() { print("Hello, ") val job = GlobalScope.launch { delay(1000L) println("World!") } runBlocking { job.join() } } Ein blockierender Coroutine Builder
  25. RunBlockingDemo.kt import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun main() =

    runBlocking<Unit> { print("Hello, ") launch { delay(1000L) println("World!") } } Brücke zwischen blockierender und unterbrechender Welt
  26. SeveralCoroutines.kt fun main() { runBlocking { for (i in 1..10)

    { launch { delay(i * 1000L) println("$i finished") } } println("Already there") } println("All done") }
  27. CoroutineScope.kt fun main() = runBlocking<Unit> { coroutineScope { for (i

    in 1..10) { launch { delay(i * 1000L) println("$i finished") } } println("Already there") } println("All done") } coroutineScope ist suspending function
  28. § runBlocking und coroutineScope warten auf die Ausführung ihres Blocks

    und Kinder § runBlocking blockiert den aktuellen Thread, coroutineScope unterbricht nur die Ausführung § Auf dem aktuellen Thread können andere Aktionen ausgeführt werden
  29. Ausführung abbrechen § launch liefert Job-Objekt § Steuert den Lebenszyklus

    § Ermöglicht Hierarchien § cancel initiiert den Abbruch § Mit join auf Abschluss warten § launch ist fire and forget
  30. CancelDemo.kt fun main() = runBlocking { println(measureTimeMillis { val job

    = launch { println("Enter") delay(10000) println("Exit") } delay(3000) job.cancelAndJoin() println("Cancelled") }) }
  31. Generell gilt... § Das Abbrechen eines Scopes mit cancel() bricht

    auch die Kindjobs ab § Vorhandene Scopes z. B. unter Android führen cancel() automatisch aus § Ein mit cancel() abgebrochenes Kind führt nicht zum Abbruch anderer Kinder des Jobs
  32. CancelNotWorkingDemo.kt fun main() = runBlocking { println(measureTimeMillis { val job

    = launch { println("Enter") var count = 0 while (true) { println("${++count}") } println("Exit") } delay(3000) job.cancelAndJoin() println("Cancelled") }) }
  33. Koroutinen sind kooperativ § Regelmäßig delay(), yield() oder ensureActive() aufrufen

    § Abbruch-Wünsche mit isActive erfragen ... val job = launch { println("Enter") var count = 0 while (isActive) { println("${++count}") yield() } println("Exit") } ... • Alle suspending functions in kotlinx.coroutines sind abbrechbar • Auch eigene suspend functions sollten abbrechbar sein
  34. CancellationException § Wird von suspend functions geworfen, um Abbruch anzuzeigen

    § Mit ihr kann zwischen Abbrüchen und anderen Exceptions unterschieden werden
  35. TimeoutDemo.kt fun main(): Unit = runBlocking<Unit> { println(measureTimeMillis { val

    delay = 2000 try { withTimeout(delay + 1000L) { val current = System.currentTimeMillis() while ((System.currentTimeMillis() - current) < delay) { yield() } } } catch (e: TimeoutCancellationException) { println("I timed out") } finally { println("finally") } }) }
  36. ReturnAValueDemo.kt fun main() = runBlocking<Unit> { println("Took ${measureTimeMillis { println("result:

    ${myFun1() + myFun2()}") }} ms") } suspend fun myFun1(): Int { delay(1000) return 1 } suspend fun myFun2(): Int { delay(2000) return 2 }
  37. Deferred § Nicht blockierende, abbrechbare Future § Wird mit async

    (Builder) oder CompletableDeferred() erzeugt § Dieselben Zustände wie Job § Ergebnis wird mit await() erfragt (wirft im Fehlerfall eine Exception)
  38. AsyncDemo.kt fun main() = runBlocking<Unit> { async { waiter(2000) }

    println("Result: ${async { waiter(3000) }.await()}") } suspend fun waiter(timeToWait: Long): Long { println("Waiting for $timeToWait ms") delay(timeToWait) return timeToWait }
  39. Fibonacci.kt fun main() { val fibs = fibList(10) for (i

    in (1..fibs.size)) { println("fib($i) = ${fibs[i - 1]}") } } fun fibList(n: Int): List<Int> { val result: MutableList<Int> = ArrayList() for (i in (1..n)) { result.add(if (i <= 2) 1 else result[i - 2] + result[i - 3]) } return result }
  40. FibonacciFlow.kt fun main() = runBlocking { fibFlow(10).collectIndexed { index, value

    -> println("fib(${index + 1}) = ${value}") } } fun fibFlow(n: Int): Flow<Int> = flow { var minus2: Int var minus1 = 1 var current = 1 for (i in 1..n) { if (i > 2) { minus2 = minus1 minus1 = current current = minus1 + minus2 } emit(current) } }
  41. Flow Operatoren § transform (map reduce, Zwischenwerte, ...) § Einsammeln

    der Flow-Werte (terminal) § collect § toList und toSet § first und single § reduce und fold
  42. Fortgeschrittene Flow Konzepte § flowOn ändert Ausführungskontext § buffer ändert

    den Charakter eines F.lows von sequentiell zu nebenläufig § conflate erlaubt das Auslassen von Zwischenwerten § Multiple Flows mit zip und und combine
  43. Channels § Mit Deferred können einzelne Werte zwischen Koroutinen ausgetauscht

    werden § Channels übertragen Datenströme § Konzeptionell der BlockingQueue ähnlich
  44. ChannelDemo.kt fun main() = runBlocking<Unit> { val channel = Channel<Int>()

    val job = launch { while (isActive) { channel.send((Math.random() * 10).toInt()) } } repeat(5) { println(channel.receive()) } job.cancelAndJoin() channel.close() }
  45. ChannelDemo2.kt fun main() = runBlocking<Unit> { val channel = Channel<Int>()

    launch { repeat(5) { channel.send((Math.random() * 10).toInt()) } channel.close() } for (i in channel) { println(i) } } ChannelDemo3.kt fun main() = runBlocking<Unit> { myProducer().consumeEach { println(it) } } fun CoroutineScope.myProducer(): ReceiveChannel<Int> = produce { repeat(5) { send((Math.random() * 10).toInt()) } }
  46. Mehr mit Producern § Fan out § Mehrere Koroutinen erhalten

    Werte vom selben Channel § Teilen sich die Arbeit § Fan in § Mehrere Koroutinen senden Daten zum selben Channel § Gepufferte Channels
  47. § Ausnahmen nach launch werden wie nicht gefangene Ausnahmen in

    Threads behandelt § Abgestürzte Kind-Koroutinen brechen die Elternkoroutine mit der korrespondierenden Ausnahme ab § Bei async sollten Ausnahmen explizit behandelt werden
  48. ExceptionDemo.kt import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking fun main() = runBlocking<Unit> {

    val job = launch() { println("Hello launch") throw RuntimeException("Hello crash") } job.join() // not printed!!! println("join() finished") }
  49. ExceptionDemo3.kt fun main() = runBlocking { val deferred = GlobalScope.async

    { println("Started") delay(1000) throw IllegalArgumentException("I want to crash") } try { deferred.await() } catch (e: Exception) { println(e.message) } finally { println("I am finally here") } println("Finished") }
  50. ExceptionDemo2.kt import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.GlobalScope import ... fun main() =

    runBlocking { val handler = CoroutineExceptionHandler { _, exception -> println("Caught $exception") } val job = GlobalScope.launch(handler) { println("Hello launch") throw RuntimeException("Hello crash") } job.join() println("join() finished") }
  51. SupervisorJob § Ein Fehler oder Abbruch führt nicht zum Abbruch

    anderer Kinder bzw. des Parents val scope = CoroutineScope(SupervisorJob()) § Nicht gefangene Ausnahmen werden nach oben propagiert § SupervisorJob funktioniert nur, wenn er der direkte Parent einer Koroutine ist
  52. § Koroutinen werden durch eigene Bibliothek (kotlinx.coroutines) implementiert § Auf

    Sprachebene „nur“ ein Schlüsselwort (suspend) § Einige Features sind (noch) experimentell
  53. § Koroutinen fühlen sich wie Threads an § Verhalten sich

    aber anders § Blockierend vs. unterbrechend § Erfordern Mitarbeit des Entwicklers (kooperativ) § Aufpassen, wenn Thread-basierte Mechanismen „einfach so“ übernommen werden
  54. § API fühlt sich nicht immer intuitiv an § Wann

    welcher Dispatcher? § Wann welcher Start Mode? § Default-Werte machen Verhalten manchmal schwer nachvollziehbar
  55. Hinweise für eigene Experimente § Umgang mit veränderlichen Zustand mit

    Aktoren § Select-Ausdrücke (experimentelles Feature) § In den Code schauen, um Koroutinen genau zu verstehen