Explanation
What just happened
Four blocking calls (Thread.sleep standing in for JDBC or a file read) were launched on Dispatchers.Default, on a machine with 2 CPUs. Default has one thread per CPU, so two calls ran while the other two waited for a free thread. Both threads spent all that time blocked, not computing.
Try the what-if switch
- Dispatchers.IO: the same four calls all run at once, each on its own thread. IO is made for this: it lets blocking calls wait side by side without taking threads away from CPU work.
Rule of thumb
CPU work, like parsing or sorting: Dispatchers.Default. Blocking calls: withContext(Dispatchers.IO). Suspending calls, like delay(): any dispatcher, since they don’t hold a thread while they wait.
Watch out
Our test machine was set to 2 CPUs so the timeline is readable. On your machine Default has one thread per core, but the effect is the same once the blocking calls outnumber them.
Common questions
What's the difference between Dispatchers.IO and Dispatchers.Default?
Default has one thread per CPU core, for CPU work. IO adds threads, up to 64 by default, so blocking calls can wait side by side. They share one pool: both run on threads named DefaultDispatcher-worker-N.
Why are blocking calls bad on Dispatchers.Default?
A blocking call holds its thread while it waits. With one thread per core, a few blocking calls can take every Default thread, and all other Default coroutines wait.
How many threads does Dispatchers.IO have?
It runs at most 64 blocking calls at once by default. The 65th waits for one of them to finish.
Verified: this timeline matches a real program, checked in CI on Kotlin 2.4 with kotlinx.coroutines 1.11. How we verify. Unclear or wrong? Tell us