Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why 'withContext' does not switch coroutines under 'runBlocking'?

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
like image 495
zwlxt Avatar asked Mar 03 '23 04:03

zwlxt


2 Answers

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.

like image 137
Marko Topolnik Avatar answered Apr 27 '23 15:04

Marko Topolnik


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
like image 36
Mahdi-Malv Avatar answered Apr 27 '23 16:04

Mahdi-Malv