I want to find out the executing order and thread switching of kotlin coroutines. I used withContext
to switch to another context and run time consuming tasks, so the main thread won't be blocked. But kotlin did not switch context as expected.
Code runs on kotlin playground: https://pl.kotl.in/V0lbCU25K
Case that doesn't work
suspend fun main() = runBlocking {
println("Hello, world!!!")
println(Thread.currentThread().name)
withContext(Dispatchers.IO) {
println("Before heavy load: ${Thread.currentThread().name}")
Thread.sleep(5000)
println("After heavy load: ${Thread.currentThread().name}")
}
println("waiting")
println(Thread.currentThread().name)
}
Outputs
Hello, world!!!
main @coroutine#1
Before heavy load: DefaultDispatcher-worker-1 @coroutine#1
After heavy load: DefaultDispatcher-worker-1 @coroutine#1
waiting
main @coroutine#1
The sleep
function in the above code blocks runs at the same thread as main thread and blocks it.
Below cases match my expectation(Time consuming task does not block main thread)
Case 1
suspend fun main() = runBlocking {
println("Hello, world!!!")
println(Thread.currentThread().name)
launch {
println("Before heavy load: ${Thread.currentThread().name}")
Thread.sleep(5000)
println("After heavy load: ${Thread.currentThread().name}")
}
println("waiting")
println(Thread.currentThread().name)
}
Outputs
Hello, world!!!
main @coroutine#1
waiting
main @coroutine#1
Before heavy load: main @coroutine#2
After heavy load: main @coroutine#2
Case 2
suspend fun main() = runBlocking {
println("Hello, world!!!")
println(Thread.currentThread().name)
launch {
withContext(Dispatchers.IO) {
println("Before heavy load: ${Thread.currentThread().name}")
Thread.sleep(5000)
println("After heavy load: ${Thread.currentThread().name}")
}
}
println("waiting")
println(Thread.currentThread().name)
}
Outputs
Hello, world!!!
main @coroutine#1
waiting
main @coroutine#1
Before heavy load: DefaultDispatcher-worker-1 @coroutine#2
After heavy load: DefaultDispatcher-worker-1 @coroutine#2
I used
withContext
to switch to another context and run time consuming tasks, so the main thread won't be blocked. But kotlin did not switch context as expected.
Your withContext
call did indeed free up the main thread. It transferred the work to another thread, but at that point your main thread was left with nothing else to do but wait for the withContext
invocation to complete. runBlocking
starts an event loop that can serve any number of concurrent coroutines, but since you have only one, that one coroutine had to complete in order for the runBlocking
block to complete.
Here's a demonstration of what it means that the thread isn't blocked:
fun main() {
measureTimeMillis {
runBlocking {
launchManyCoroutines()
println("Top-level coroutine sleeping on thread ${currentThread().name}")
delay(2_000)
println("Top-level coroutine done")
}
}.also { println("Program done in $it milliseconds") }
}
private fun CoroutineScope.launchManyCoroutines() {
val cpuCount = getRuntime().availableProcessors()
(1 until cpuCount).forEach { coroId ->
launch { // on the main thread
val sum = withContext(Dispatchers.Default) {
println("Coroutine #$coroId computing on thread ${currentThread().name}")
computeResult()
}
println("Coroutine #$coroId done on thread ${currentThread().name}:" +
" sum = $sum")
}
}
(cpuCount + 1..100).forEach { coroId ->
launch { // on the main thread
println("Coroutine $coroId sleeping 1 s on thread ${currentThread().name}")
delay(1_000)
println("Coroutine #$coroId done on thread ${currentThread().name}")
}
}
}
private fun computeResult(): Int {
val rnd = ThreadLocalRandom.current()
return (1..1_000_000).map { rnd.nextInt() }.sum()
}
This program launches (100 + availableProcessors) concurrent coroutines, all on the main thread. Some of them use withContext(Dispatchers.Default)
to perform a CPU-intensive task (summing a million random integers) on a thread pool, while others perform suspending work (delay for one second) directly on the main thread. Finally, the top-level coroutine sleeps for 2 seconds before completing.
The entire program completes in just above 2 seconds and prints something like the following:
Top-level coroutine sleeping on thread main
Coroutine #2 computing on thread DefaultDispatcher-worker-2
Coroutine #3 computing on thread DefaultDispatcher-worker-3
Coroutine #4 computing on thread DefaultDispatcher-worker-5
Coroutine #1 computing on thread DefaultDispatcher-worker-1
Coroutine #5 computing on thread DefaultDispatcher-worker-6
Coroutine #6 computing on thread DefaultDispatcher-worker-4
Coroutine #7 computing on thread DefaultDispatcher-worker-8
Coroutine 9 sleeping 1 s on thread main
Coroutine 10 sleeping 1 s on thread main
...
Coroutine 99 sleeping 1 s on thread main
Coroutine 100 sleeping 1 s on thread main
Coroutine #3 done on thread main: sum = -1248358970
Coroutine #4 done on thread main: sum = -228252033
Coroutine #6 done on thread main: sum = -147126590
Coroutine #2 done on thread main: sum = -1065374439
Coroutine #1 done on thread main: sum = -2029316381
Coroutine #7 done on thread main: sum = -865387844
Coroutine #5 done on thread main: sum = -1695642504
Coroutine #9 done on thread main
Coroutine #10 done on thread main
...
Coroutine #99 done on thread main
Coroutine #100 done on thread main
Top-level coroutine done
Program done in 2066 milliseconds
Note than everything the program does, happens while the main coroutine is sleeping on the main thread.
TL;DR
Use launch
withContext
doesn't do async. It merges the contexts. To do an async job use the launch function with the specified coroutine context.
withContext
does not actually run async, it merges the contexts.
According to the Kotlin core docs:
withContext
:
Calls the specified suspending block with a given coroutine context, suspends until it completes, and returns the result.
So the expected result of this code using withContext
:
fun main() = runBlocking {
println("Before block ${Thread.currentThread().name}")
withContext(Dispatchers.IO) {
println("Long op ${Thread.currentThread().name}")
delay(1000)
}
println("After block")
}
is:
Before block main @coroutine#1
Long op DefaultDispatcher-worker-1 @coroutine#1
After block
What you want can be achieved using launch
function inside your coroutine:
The result of the below code:
fun main() = runBlocking {
println("Before block ${Thread.currentThread().name}")
launch(Dispatchers.IO) {
println("Long op ${Thread.currentThread().name}")
delay(1000)
}
println("After block")
}
will be:
Before block main @coroutine#1
After block
Long op DefaultDispatcher-worker-1 @coroutine#2
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With