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

Kotlin Coroutine Mechanisms: A Surprisingly Dee...

Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole

Sometimes you think you know coroutines, and then after a while, you’re like, “Wait, do I really know coroutines?” Inspired by the O’Reilly book _Programming Android with Kotlin_ and the upcoming new Droidcon eBook _Mastering Kotlin Coroutines: From Basics to Advanced _, this talk strengthens everyday coroutine understanding through playful explorations. You don't have to master coroutines to get started and be productive with coroutines. But when you walk out of this talk, you have deep understanding of coroutine mechanics and what tools can help you debug coroutine behavior. You might be in the beginning stages of learning Kotlin. Or maybe you’ve been using coroutines for a while and want to brush up, or maybe you're a little burned from other talks. Either way, you'll be looking at coroutines a little differently by the end of this session!

Want more content? Start here https://codingkinetics.com/dcnyc2025

Avatar for mvndy_hd

mvndy_hd

June 25, 2025
Tweet

More Decks by mvndy_hd

Other Decks in Technology

Transcript

  1. Droidcon NYC 2025 Kotlin Coroutine Mechanisms Amanda Hinchman-Dominguez Android |

    coauthor of Programming Android with Kotlin: Achieving Structured Concurrency with Coroutines | Coding Kinetics | co-org of Chicago KUG A surprisingly deep rabbithole
  2. GlobalScope.launch EVERYTHING Why did it do that Scope it right.

    Structure concurrency. Profit. The average developer journey
  3. Sure, Kotlin Coroutines has a few warts i.e. existing tooling,

    testing, API edges Most pain points come from a mismatch between developer expectations and coroutine design androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  4. Following the rules of structured concurrency will solve the majority

    of your coroutine problems androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  5. TABLE OF CONTENTS 01. WHAT COROUTINES SOLVE COROUTINE SCOPE &

    CONTEXT 02. THE STRUCTURED CONCURRENCY PARADIGM COROUTINE CANCELLATION MECHANISMS androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  6. Problem 1 Threads are expensive • Expensive to: ◦ Spin

    up ◦ Cancel ◦ Context-switch • Android has a lit on how many threads can run concurrently • The OS controls thread scheduling and not you :( ◦ Forcing a thread to stop requires OS-level interruption androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  7. Problem 1 Threads are expensive Threads are blocking. A thread

    can only run one task at a time. • Must use multiple threads to achieve parallel execution androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  8. Problem 1 Threads are expensive androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Blocking

    tasks in threads can choke a program via: • Long-running operations without cancellation • Resource starvation
  9. Solution 1 Coroutines are cheaper androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman •

    Coroutines can run multiple tasks asynchronously in a single thread •
  10. Problem 2 Threads are hard to manage Just because you

    ask a thread to stop doesn't mean it will do it when you ask it to. Threads are not immediately responsive to cancellation requests androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  11. Problem 3 Each thread has its own state and timeline

    Thread Cooperatio n is Prone to Side-effect s Multiple threads accessing mutable state can produce side effects difficult to control • Shared resources - threads write to the same variable/file/resource • Side effects are the root cause ◦ Race conditions ◦ Deadlocks ◦ Hard-to-reproduce bugs androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  12. Problem 3 Each thread has its own state and timeline

    Thread Cooperatio n is Prone to Side-effect s androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  13. Problem 3 Coroutines can produce side-effect s (depending on setup)

    Multiple coroutines accessing the same shared resource can also result in unintended side-effects androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  14. Problem 3 Nulla porttitor massa id neque. Structured Concurrenc y

    contains side-effect s androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Structured Concurrency helps to contain unintended side-effects
  15. Structured Concurrenc y INTRODUCTION TO 1. A PARENT COROUTINE ALWAYS

    WAITS FOR ITS CHILDREN TO COMPLETE androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman 2. A CHILD COROUTINE NEVER GETS LOST
  16. Structured Concurrency 1. A coroutine always waits on child completion

    androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman • A structured concurrency system waits for all tasks to finish before leaving the scope. • No task is left incomplete or abandoned. • This guarantees that your app behaves predictably and gracefully.
  17. Problem 3 2. A child coroutine never gets lost androiddev.social/@amanda

    #dcnyc25 [email protected] AmandaHinchman By default, when a child coroutine fails • It fails silently • Cancels all of its child coroutines • Does not propagate the cancellation back up
  18. Problem 3 2. A child coroutine never gets lost androiddev.social/@amanda

    #dcnyc25 [email protected] AmandaHinchman • Structured concurrency addresses problems of lost failures/exceptions in child coroutines • The Kotlin Coroutines API offers a few cancellation mechanisms to ensure that a child coroutine returns completion even if it fails
  19. Structured Concurrenc y INTRODUCTION TO Coroutine Scopes and Contexts androiddev.social/@amanda

    #dcnyc25 [email protected] AmandaHinchman Coroutine Cancellation Mechanisms 1. A PARENT COROUTINE ALWAYS WAITS FOR ITS CHILDREN TO COMPLETE 2. A CHILD COROUTINE NEVER GETS LOST
  20. @kotlin.SinceKotlin public interface CoroutineContext { public abstract operator fun <E

    : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? public abstract fun <R> fold( initial: R, operation: (R, CoroutineContext.Element) *> R ): R public open operator fun plus( context: CoroutineContext ): CoroutineContext { ** compiled code */ } public abstract fun minusKey(key: CoroutineContext.Key**>): CoroutineContext public interface Key<E : CoroutineContext.Element> { } public interface Element : CoroutineContext { public abstract val key: CoroutineContext.Key** public open operator fun <E : CoroutineContext.Element> get(**.): E? { ** compiled code */ } public open fun <R> fold(initial: R, operation: (R, CoroutineContext.Element) *> R): R { ** compiled code */ } public open fun minusKey(key: kotlin.coroutines.CoroutineContext.Key**>): kotlin.coroutines.CoroutineContext { ** compiled code */ } }
  21. androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman CoroutineScope v. CoroutineContext • CoroutineScope -

    container for CoroutineContext ◦ It defines a group of coroutine jobs working together • CoroutineContext - set of elements that describes coroutine behavior ◦ Dispatcher ◦ Job ◦ Coroutine name + other optional information
  22. Elizarov, Roman. “Coroutine Context and Scope.” Medium, 9 Mar. 2019,

    https://elizarov.medium.com/coroutine-context-and-scope-c8b255d59055.
  23. Problem 3 androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Job Hierarchy • When

    a Job is launched in another Job, they create in a child-parent hierarchy • Every coroutine has a clear parent-child relationship within a scope.
  24. Problem 3 androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Coroutine Scope in a

    hierarchy • A new CoroutineScope(...) does NOT inherit the parent scope just because it is launched in another scope
  25. Coroutines playground A new scope breaks structured concurrenc y fun

    main() = runBlocking { log("main runBlocking") val job = launch { log(" job launched") val task1 = async { log(" task1 launch") delay(6000) " task1 complete" } log(task1.await()) } log("Start job ") job.join() log("Program ends ") } androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  26. Coroutines playground A new scope breaks structured concurrenc y fun

    main() = runBlocking { log("main runBlocking") val job = launch { log(" job launched") val task1 = async { log(" task1 launch") delay(6000) " task1 complete" } log(task1.await()) } log("Start job ") job.join() log("Program ends ") } androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  27. Coroutines playground A new scope breaks structured concurrenc y fun

    main() = runBlocking { log("main runBlocking") val scope = CoroutineScope(Job()) val job = launch { log(" job launched") val task1 = scope.async { log(" task1 launch") delay(6000) " task1 complete" } log(task1.await()) } log("Start job ") job.join() log("Program ends ") } androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  28. Not all scopes are created equal androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman

    CoroutineScope(...).launch { ... } coroutineScope { ... } GlobalScope.launch { ... } viewModelScope.launch { ... } lifecycleScope.launch Respects structured concurrency? Inherited or new scope?
  29. CoroutineScope(...).launch { ... } No New scope coroutineScope { ...

    } Yes Inherits parent scope GlobalScope.launch { ... } No Global scope viewModelScope.launch { ... } Yes Inherits from ViewModel lifecycleScope.launch { ... } Yes Inherits from lifecycle object Not all scopes are created equal androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Respects structured concurrency? Inherited or new scope?
  30. CoroutineScope(...).launch { ... } No New scope coroutineScope { ...

    } Yes Inherits parent scope GlobalScope.launch { ... } No Global scope viewModelScope.launch { ... } Yes Inherits from ViewModel lifecycleScope.launch { ... } Yes Inherits from lifecycle object Not all scopes are created equal androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman Respects structured concurrency? Inherited or new scope?
  31. CoroutineScope extension functions can respects structured concurrency androiddev.social/@amanda #dcnyc25 [email protected]

    AmandaHinchman fun CoroutineScope.someExtensionFunction(number: Int) { /** removed for brevity **/ }
  32. Automated cancellatio n mechanism s androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman •

    cancelAndJoin( ) • withTimeout { ... } ◦ cancels and throws TimeoutCancellationException • withTimeoutOrNull { ...} ◦ throws null instead
  33. Cleanup Jobs with try/finally fun main() = runBlocking { log("main

    runBlocking ") val job = launch { try { log(" job launched ") launchTask(1).join() launchTask(2).join() launchTask(3).join() launchTask(4).join() } finally { delay(100) log("Cleaning up resources before exit") } } log("Delay ") delay(1500) job.cancelAndJoin() log("Program ends ") } androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  34. withContext(NonCancellable) fun main() = runBlocking { log("main runBlocking ") val

    job = launch { try { log(" job launched ") launchTask(1).join() launchTask(2).join() launchTask(3).join() launchTask(4).join() } finally { withContext(NonCancellable) { delay(100) log("Cleaning up resources before exit") } } } log("Delay ") delay(1500) job.cancelAndJoin() log("Program ends ") } androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  35. Default cancellation behavior for unhandled exceptions androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman

    • When an unhandled coroutine exception occurs the job cancels • The exception propagates upward to the scope, causing the scope and all its jobs to cancel • Future tasks don't start or get created since the scope is canceled.
  36. SupervisorJob to the rescue androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman • Scope

    stays active, job2 launches • job1 cancels since the exception is unhandled
  37. • Exposed exceptions ◦ The only kinds of exceptions which

    can be handled by client code try/catch • Unhandled exceptions ◦ Typically makes your application crash (or at least, recovering from them might leave the app in an undetermined state) ◦ Can also leave unintended side-effects in child coroutines since those fail silently What kind of Coroutine exceptions are there? androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  38. Problem 3 Handling unhandled exceptions by registering a CoroutineExceptionHandl er

    • Stops exception propagation • Avoids a program crash androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  39. 52 Problem 3 Cymbal Visual Identity Guidelines Month 20XX Handling

    unhandled exceptions by registering a CoroutineExceptionHandl er No leaks, and waits for completion before cleanup - expresses uncaught exceptions Register CEH to: ◦ launch must be coroutine root builder ◦ a scope ◦ a supervisorScope child androiddev.social/@amanda #dcnyc25 [email protected] AmandaHinchman
  40. Why is the mental model around concurrency so hard? 54

    TYPOGRAPHY Cymbal Visual Identity Guidelines Month 20XX DM SERIF DISPLAY It's not you. It could be linguistics. • Concurrency - dynamic model expressed in time • Western linguitics often references time as static references (past/present/future)