Slide 1

Slide 1 text

elizarov@ Roman Elizarov Kotlin Coroutines in Practice @relizarov

Slide 2

Slide 2 text

Coroutines recap

Slide 3

Slide 3 text

Coroutines are like light-weight threads

Slide 4

Slide 4 text

suspend fun main() { val jobs = List(100_000) { GlobalScope.launch { delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads

Slide 5

Slide 5 text

suspend fun main() { val jobs = List(100_000) { GlobalScope.launch { delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads

Slide 6

Slide 6 text

suspend fun main() { val jobs = List(100_000) { GlobalScope.launch { delay(5000) print(".") } } jobs.forEach { it.join() } } Coroutines are like light-weight threads

Slide 7

Slide 7 text

Quantity

Slide 8

Slide 8 text

Quantity → Quality

Slide 9

Slide 9 text

A practical challenge suspend fun downloadContent(location: Location): Content

Slide 10

Slide 10 text

fun processReferences(refs: List) References

Slide 11

Slide 11 text

References fun processReferences(refs: List) { for (ref in refs) { … } }

Slide 12

Slide 12 text

References Locations resolve fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() … } }

Slide 13

Slide 13 text

References Locations Contents resolve download fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } }

Slide 14

Slide 14 text

References Locations Contents resolve download suspend fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } }

Slide 15

Slide 15 text

References Locations Contents resolve download suspend fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } Sequential

Slide 16

Slide 16 text

References Locations Contents resolve download fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() val content = downloadContent(location) processContent(ref, content) } } Parallel

Slide 17

Slide 17 text

References Locations Contents resolve download fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } Parallel

Slide 18

Slide 18 text

fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } Coroutines are cheap! What could go wrong?

Slide 19

Slide 19 text

fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } } ref1 location1 Launch download ref2 location2 Launch download ref3 Crash! Crash!

Slide 20

Slide 20 text

ref1 location1 Launch download ref2 location2 Launch download ref3 Crash! Crash! Leak! fun processReferences(refs: List) { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } }

Slide 21

Slide 21 text

Structured concurrency

Slide 22

Slide 22 text

fun processReferences(refs: List)

Slide 23

Slide 23 text

suspend fun processReferences(refs: List)

Slide 24

Slide 24 text

suspend fun processReferences(refs: List) = coroutineScope { … }

Slide 25

Slide 25 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() GlobalScope.launch { val content = downloadContent(location) processContent(ref, content) } } }

Slide 26

Slide 26 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } }

Slide 27

Slide 27 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Child

Slide 28

Slide 28 text

Crash? suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } }

Slide 29

Slide 29 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Crash?

Slide 30

Slide 30 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Crash? cancels

Slide 31

Slide 31 text

Crash? cancels Waits for completion suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } }

Slide 32

Slide 32 text

suspend fun processReferences(refs: List) = coroutineScope { for (ref in refs) { val location = ref.resolveLocation() launch { val content = downloadContent(location) processContent(ref, content) } } } Never leaks jobs

Slide 33

Slide 33 text

The state

Slide 34

Slide 34 text

References Contents Download process

Slide 35

Slide 35 text

Reference 1 Content Reference 2 Location Download process State

Slide 36

Slide 36 text

class Downloader { }

Slide 37

Slide 37 text

class Downloader { private val requested = mutableSetOf() }

Slide 38

Slide 38 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() … } }

Slide 39

Slide 39 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } } }

Slide 40

Slide 40 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 41

Slide 41 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Concurrent

Slide 42

Slide 42 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Concurrent

Slide 43

Slide 43 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Shared mutable state

Slide 44

Slide 44 text

class Downloader { private val requested = mutableSetOf() fun downloadReference(ref: Reference) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } } Shared mutable state Needs Synchronization Shared + Mutable =

Slide 45

Slide 45 text

Shared Mutable State Share by Communicating

Slide 46

Slide 46 text

Synchronization Primitives Communication Primitives

Slide 47

Slide 47 text

classes coroutines

Slide 48

Slide 48 text

launch { val requested = mutableSetOf() … } Does not share mutable state

Slide 49

Slide 49 text

launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 50

Slide 50 text

launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 51

Slide 51 text

Channel References Downloader

Slide 52

Slide 52 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { … }

Slide 53

Slide 53 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { … }

Slide 54

Slide 54 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { … }

Slide 55

Slide 55 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { … } Convention

Slide 56

Slide 56 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 57

Slide 57 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 58

Slide 58 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 59

Slide 59 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { // schedule download } // ... wait for result ... processContent(ref, content) } }

Slide 60

Slide 60 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Coroutines are cheap! What could go wrong?

Slide 61

Slide 61 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Child

Slide 62

Slide 62 text

fun CoroutineScope.downloader( references: ReceiveChannel, ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { launch { … } } // ... wait for result ... processContent(ref, content) } } Coroutines are cheap! But the work they do…

Slide 63

Slide 63 text

Limiting concurrency

Slide 64

Slide 64 text

Worker 1 Worker 2 Worker 3 Worker N Worker pool … References Downloader references locations

Slide 65

Slide 65 text

fun CoroutineScope.downloader( references: ReceiveChannel, locations: SendChannel )

Slide 66

Slide 66 text

fun CoroutineScope.downloader( references: ReceiveChannel, locations: SendChannel ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { locations.send(location) } } }

Slide 67

Slide 67 text

fun CoroutineScope.worker( locations: ReceiveChannel )

Slide 68

Slide 68 text

fun CoroutineScope.worker( locations: ReceiveChannel )

Slide 69

Slide 69 text

fun CoroutineScope.worker( locations: ReceiveChannel ) = launch { for (loc in locations) { val content = downloadContent(loc) processContent(ref, content) } }

Slide 70

Slide 70 text

fun CoroutineScope.worker( locations: ReceiveChannel ) = launch { for (loc in locations) { val content = downloadContent(loc) processContent(ref, content) } } Fan-out

Slide 71

Slide 71 text

fun CoroutineScope.worker( locations: ReceiveChannel ) = launch { for (loc in locations) { val content = downloadContent(location) processContent(ref, content) } }

Slide 72

Slide 72 text

fun CoroutineScope.worker( locations: ReceiveChannel ) = launch { for (loc in locations) { val content = downloadContent(loc) processContent(ref, content) } }

Slide 73

Slide 73 text

fun CoroutineScope.worker( locations: ReceiveChannel ) = launch { for (loc in locations) { val content = downloadContent(loc) processContent(ref, content) } }

Slide 74

Slide 74 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader

Slide 75

Slide 75 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader Refs ↔ Locs location & content

Slide 76

Slide 76 text

data class LocContent(val loc: Location, val content: Content)

Slide 77

Slide 77 text

data class LocContent(val loc: Location, val content: Content) fun CoroutineScope.worker( locations: ReceiveChannel, contents: SendChannel )

Slide 78

Slide 78 text

data class LocContent(val loc: Location, val content: Content) fun CoroutineScope.worker( locations: ReceiveChannel, contents: SendChannel ) = launch { for (loc in locations) { val content = downloadContent(loc) contents.send(LocContent(loc, content)) } }

Slide 79

Slide 79 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents

Slide 80

Slide 80 text

fun CoroutineScope.downloader( references: ReceiveChannel, locations: SendChannel, contents: ReceiveChannel )

Slide 81

Slide 81 text

fun CoroutineScope.downloader( references: ReceiveChannel, locations: SendChannel, contents: ReceiveChannel ) = launch { val requested = mutableSetOf() for (ref in references) { val location = ref.resolveLocation() if (requested.add(location)) { locations.send(location) } } } Hmm….

Slide 82

Slide 82 text

Select

Slide 83

Slide 83 text

select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } }

Slide 84

Slide 84 text

select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } }

Slide 85

Slide 85 text

select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } }

Slide 86

Slide 86 text

launch { val requested = mutableMapOf>() … }

Slide 87

Slide 87 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } } } }

Slide 88

Slide 88 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> … } } } }

Slide 89

Slide 89 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> val loc = ref.resolveLocation() … } contents.onReceive { (loc, content) -> … } } } }

Slide 90

Slide 90 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] … } contents.onReceive { (loc, content) -> … } } } }

Slide 91

Slide 91 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } } contents.onReceive { (loc, content) -> … } } } }

Slide 92

Slide 92 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } else { refs.add(ref) } } contents.onReceive { (loc, content) -> … } } } }

Slide 93

Slide 93 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> val loc = ref.resolveLocation() val refs = requested[loc] if (refs == null) { requested[loc] = mutableListOf(ref) locations.send(loc) } else { refs.add(ref) } } contents.onReceive { (loc, content) -> … } } } } No concurrency No synchronization

Slide 94

Slide 94 text

launch { val requested = mutableMapOf>() while (true) { select { references.onReceive { ref -> … } contents.onReceive { (loc, content) -> val refs = requested.remove(loc)!! for (ref in refs) { processContent(ref, content) } } } } }

Slide 95

Slide 95 text

Putting it all together

Slide 96

Slide 96 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents references

Slide 97

Slide 97 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents references

Slide 98

Slide 98 text

fun CoroutineScope.processReferences( references: ReceiveChannel )

Slide 99

Slide 99 text

fun CoroutineScope.processReferences( references: ReceiveChannel )

Slide 100

Slide 100 text

fun CoroutineScope.processReferences( references: ReceiveChannel ) { val locations = Channel() val contents = Channel() repeat(N_WORKERS) { worker(locations, contents) } downloader(references, locations, contents) }

Slide 101

Slide 101 text

fun CoroutineScope.processReferences( references: ReceiveChannel ) { val locations = Channel() val contents = Channel() repeat(N_WORKERS) { worker(locations, contents) } downloader(references, locations, contents) }

Slide 102

Slide 102 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents references

Slide 103

Slide 103 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents references processReferences

Slide 104

Slide 104 text

Worker 1 Worker 2 Worker 3 Worker N … References Downloader locations contents references processReferences Patterns everywhere Worker pool Actor

Slide 105

Slide 105 text

fun CoroutineScope.processReferences(…)

Slide 106

Slide 106 text

Root CoroutineScope

Slide 107

Slide 107 text

class SomethingWithLifecycle { }

Slide 108

Slide 108 text

class SomethingWithLifecycle : CoroutineScope { }

Slide 109

Slide 109 text

class SomethingWithLifecycle : CoroutineScope { override val coroutineContext: CoroutineContext get() = … }

Slide 110

Slide 110 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() override val coroutineContext: CoroutineContext get() = … }

Slide 111

Slide 111 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() fun dispose() { … } override val coroutineContext: CoroutineContext get() = … }

Slide 112

Slide 112 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() fun close() { … } override val coroutineContext: CoroutineContext get() = … }

Slide 113

Slide 113 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = … }

Slide 114

Slide 114 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = job }

Slide 115

Slide 115 text

class SomethingWithLifecycle : CoroutineScope { private val job = Job() fun close() { job.cancel() } override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main }

Slide 116

Slide 116 text

class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main fun doSomething() { } }

Slide 117

Slide 117 text

class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main fun doSomething() { launch { … } } }

Slide 118

Slide 118 text

class SomethingWithLifecycle : CoroutineScope { … override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main fun doSomething() { processReferences(references) } } Never leak any coroutines

Slide 119

Slide 119 text

suspend vs scope

Slide 120

Slide 120 text

suspend fun downloadContent(location: Location): Content

Slide 121

Slide 121 text

suspend fun downloadContent(location: Location): Content Does something long & waits for it to complete without blocking

Slide 122

Slide 122 text

suspend fun downloadContent(location: Location): Content fun CoroutineScope.processReferences(…)

Slide 123

Slide 123 text

suspend fun downloadContent(location: Location): Content fun CoroutineScope.processReferences(…) Launches new coroutines & quickly returns, does not wait for them

Slide 124

Slide 124 text

Takeaway

Slide 125

Slide 125 text

Coroutines are like light-weight threads

Slide 126

Slide 126 text

Coroutines are NOT like threads

Slide 127

Slide 127 text

Coroutines are NOT like threads Rethink the way you structure your code

Slide 128

Slide 128 text

Thank you Any questions? elizarov@ Roman Elizarov @relizarov