Scopes and Dispatchers

CoroutineScope, Dispatchers and Structured Concurrency

A CoroutineScope carries a Job, the parent of every coroutine started in it, and a dispatcher, which picks the threads. withContext(dispatcher) runs a block on other threads and suspends the caller until it returns, which makes a function main-safe. coroutineScope { } returns only when all its children finish.

dispatchers.kt: withContext moves work, coroutineScope waits for its childrenKotlin
import kotlinx.coroutines.*
suspend fun coverBytes(book: Book): Int = withContext(Dispatchers.IO) {
  log("reading cover of ${book.title}"); Thread.sleep(200); book.title.length * 1024
}
fun main() = runBlocking {
  val sizes = coroutineScope { catalog.take(2).map { async { coverBytes(it) } }.awaitAll() }
  val best = withContext(Dispatchers.Default) { log("ranking"); catalog.maxBy { it.rating } }
  log("covers $sizes, best rated ${best.title}")
}
Output
   0 ms [DefaultDispatcher-worker-1 @coroutine#2] reading cover of The Quiet Harbor
   0 ms [DefaultDispatcher-worker-3 @coroutine#3] reading cover of Patterns of the Deep Web
 239 ms [DefaultDispatcher-worker-3 @coroutine#1] ranking
 241 ms [main @coroutine#1] covers [16384, 24576], best rated Salt and Saffron

The blocking reads ran on two workers at once, and coroutine#1 hopped to a worker and back. Dispatchers.Default has a thread per core (at least two) for CPU work; Dispatchers.IO shares them and grows to 64 or more for blocking calls; Dispatchers.Main is the UI thread. viewModelScope (ViewModel and UDF) runs on Dispatchers.Main.immediate and is cancelled with its ViewModel. Avoid GlobalScope, and inject dispatchers so tests can swap them.