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.
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}")
}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.