$30 off During Our Annual Pro Sale. View Details »

Kotlin Coroutines in Practice @ KotlinConf 2018

Kotlin Coroutines in Practice @ KotlinConf 2018

Let's see how Kotlin Coroutines are used to solve real-life concurrency and coordination problems. With coroutines we don't have to worry about shared mutable state and synchronization. We can solve the problems we face using a number of communicating coroutines, where each piece of state is confined to a single coroutine.

Roman Elizarov

October 05, 2018
Tweet

More Decks by Roman Elizarov

Other Decks in Programming

Transcript

  1. elizarov@
    Roman Elizarov
    Kotlin Coroutines in
    Practice
    @relizarov

    View Slide

  2. Coroutines recap

    View Slide

  3. Coroutines are like
    light-weight threads

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  7. Quantity

    View Slide

  8. Quantity → Quality

    View Slide

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

    View Slide

  10. fun processReferences(refs: List)
    References

    View Slide

  11. References
    fun processReferences(refs: List) {
    for (ref in refs) {

    }
    }

    View Slide

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

    }
    }

    View Slide

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

    View Slide

  14. 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)
    }
    }

    View Slide

  15. 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

    View Slide

  16. 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

    View Slide

  17. 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

    View Slide

  18. 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?

    View Slide

  19. 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!

    View Slide

  20. 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)
    }
    }
    }

    View Slide

  21. Structured
    concurrency

    View Slide

  22. fun processReferences(refs: List)

    View Slide

  23. suspend fun processReferences(refs: List)

    View Slide

  24. suspend fun processReferences(refs: List) =
    coroutineScope {

    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  31. 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)
    }
    }
    }

    View Slide

  32. 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

    View Slide

  33. The state

    View Slide

  34. References Contents
    Download process

    View Slide

  35. Reference 1
    Content
    Reference 2
    Location
    Download process
    State

    View Slide

  36. class Downloader {
    }

    View Slide

  37. class Downloader {
    private val requested = mutableSetOf()
    }

    View Slide

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

    }
    }

    View Slide

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

    View Slide

  40. 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)
    }
    }

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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

    View Slide

  44. 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 =

    View Slide

  45. Shared
    Mutable State
    Share by
    Communicating

    View Slide

  46. Synchronization
    Primitives
    Communication
    Primitives

    View Slide

  47. classes coroutines

    View Slide

  48. launch {
    val requested = mutableSetOf()

    }
    Does not share mutable state

    View Slide

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

    View Slide

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

    View Slide

  51. Channel
    References
    Downloader

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    }

    View Slide

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

    }
    Convention

    View Slide

  56. 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)
    }
    }

    View Slide

  57. 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)
    }
    }

    View Slide

  58. 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)
    }
    }

    View Slide

  59. 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)
    }
    }

    View Slide

  60. 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?

    View Slide

  61. 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

    View Slide

  62. 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…

    View Slide

  63. Limiting
    concurrency

    View Slide

  64. Worker 1
    Worker 2
    Worker 3
    Worker N
    Worker pool

    References
    Downloader
    references locations

    View Slide

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

    View Slide

  66. 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)
    }
    }
    }

    View Slide

  67. fun CoroutineScope.worker(
    locations: ReceiveChannel
    )

    View Slide

  68. fun CoroutineScope.worker(
    locations: ReceiveChannel
    )

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  74. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader

    View Slide

  75. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    Refs ↔ Locs
    location &
    content

    View Slide

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

    View Slide

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

    View Slide

  78. 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))
    }
    }

    View Slide

  79. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents

    View Slide

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

    View Slide

  81. 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….

    View Slide

  82. Select

    View Slide

  83. select {
    references.onReceive { ref ->

    }
    contents.onReceive { (loc, content) ->

    }
    }

    View Slide

  84. select {
    references.onReceive { ref ->

    }
    contents.onReceive { (loc, content) ->

    }
    }

    View Slide

  85. select {
    references.onReceive { ref ->

    }
    contents.onReceive { (loc, content) ->

    }
    }

    View Slide

  86. launch {
    val requested = mutableMapOf>()

    }

    View Slide

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

    View Slide

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

    View Slide

  89. launch {
    val requested = mutableMapOf>()
    while (true) {
    select {
    references.onReceive { ref ->
    val loc = ref.resolveLocation()

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

    View Slide

  90. launch {
    val requested = mutableMapOf>()
    while (true) {
    select {
    references.onReceive { ref ->
    val loc = ref.resolveLocation()
    val refs = requested[loc]

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

    View Slide

  91. 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) -> … }
    }
    }
    }

    View Slide

  92. 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) -> … }
    }
    }
    }

    View Slide

  93. 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

    View Slide

  94. 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)
    }
    }
    }
    }
    }

    View Slide

  95. Putting it all
    together

    View Slide

  96. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents
    references

    View Slide

  97. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents
    references

    View Slide

  98. fun CoroutineScope.processReferences(
    references: ReceiveChannel
    )

    View Slide

  99. fun CoroutineScope.processReferences(
    references: ReceiveChannel
    )

    View Slide

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

    View Slide

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

    View Slide

  102. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents
    references

    View Slide

  103. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents
    references
    processReferences

    View Slide

  104. Worker 1
    Worker 2
    Worker 3
    Worker N

    References
    Downloader
    locations
    contents
    references
    processReferences
    Patterns
    everywhere Worker pool
    Actor

    View Slide

  105. fun CoroutineScope.processReferences(…)

    View Slide

  106. Root
    CoroutineScope

    View Slide

  107. class SomethingWithLifecycle {
    }

    View Slide

  108. class SomethingWithLifecycle : CoroutineScope {
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  116. class SomethingWithLifecycle : CoroutineScope {

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

    View Slide

  117. class SomethingWithLifecycle : CoroutineScope {

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

    View Slide

  118. class SomethingWithLifecycle : CoroutineScope {

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

    View Slide

  119. suspend vs scope

    View Slide

  120. suspend fun downloadContent(location: Location): Content

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  124. Takeaway

    View Slide

  125. Coroutines are like
    light-weight threads

    View Slide

  126. Coroutines are NOT like
    threads

    View Slide

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

    View Slide

  128. Thank you
    Any questions?
    elizarov@
    Roman Elizarov
    @relizarov

    View Slide