Structured Concurrency

Why Android Needs Structured Concurrency

An app's main thread runs your code and draws every frame, one each 16 ms at 60 Hz. Block it for 500 ms and about 30 frames drop; ignore input for 5 seconds and Android shows an Application Not Responding dialog. Callbacks and AsyncTask (deprecated in API 30) moved work off it, but let the work outlive its screen, finishing for an Activity that rotation had destroyed.

A blocking call freezes the main thread; a suspended coroutine lets it keep drawing
A blocking call freezes the main thread; a suspended coroutine lets it keep drawing

A coroutine that calls a suspending function, such as delay or a Retrofit 43,944 request, gives its thread back until the result arrives:

why.kt: blocking waits add up, suspended waits overlapKotlin
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis
fun main() {
  println("3 blocking waits: ${measureTimeMillis { repeat(3) { Thread.sleep(500) } }} ms")
  val t = measureTimeMillis { runBlocking { repeat(100_000) { launch { delay(500) } } } }
  println("100,000 suspended waits: $t ms")
}
Output
$ kotlinc *.kt -cp kotlinx-coroutines-core-jvm-1.11.0.jar -include-runtime -d why.jar
$ java -cp why.jar:kotlinx-coroutines-core-jvm-1.11.0.jar WhyKt
3 blocking waits: 1500 ms
100,000 suspended waits: 1208 ms

A hundred thousand threads would need gigabytes of stacks; coroutines are small heap objects. And with structured concurrency, every coroutine starts in a CoroutineScope that waits for its children and cancels them all when it is cancelled: tie the scope to a screen and nothing outlives it.