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 coroutine that calls a suspending function, such as delay or a Retrofit 43,944 request, gives its thread back until the result arrives:
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")
}$ 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.