Home > Enterprise >  Why isn't withContext resuming to the original dispatcher on completion?
Why isn't withContext resuming to the original dispatcher on completion?

Time:03-29

I'm writing a suspending function that should run on a specific dispatcher. It may be called from any dispatcher, so it should resume to the original dispatcher when returning.

withContext should help, since it states that:

This function uses dispatcher from the new context, shifting execution of the block into the different thread if a new dispatcher is specified, and back to the original dispatcher when it completes.

Here is a basic test:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.setMain
import kotlinx.coroutines.withContext

fun log(message: String = "") {
    println("[${Thread.currentThread().name}] $message")
}

suspend fun test9Odd(x: Int): Boolean {
    return withContext(Dispatchers.IO) {
        log("test9Odd for x = $x...")
        delay(250)
        x and 1 == 1
    }
}

fun main(args: Array<String>) {
    // setting Dispatcher.Main for the test:
    val x = TestCoroutineDispatcher()
    x.runCurrent()
    Dispatchers.setMain(x)

    runBlocking {
        withContext(Dispatchers.Main) {
            log("test9")
            for (x in listOf(8, 97, 108)) {
                val result = test9Odd(x)
                log("Is $x odd? ${if (result) "yes" else "no"}")
            }
        }
    }
}

What I observe is contradictory to the statement above, the context switches but is not resumed when returning, it keeps running on worker-1:

    [main] test9
    [DefaultDispatcher-worker-1] test9Odd for x = 8...
    [DefaultDispatcher-worker-1] Is 8 odd? no
    [DefaultDispatcher-worker-1] test9Odd for x = 97...
    [DefaultDispatcher-worker-1] Is 97 odd? yes
    [DefaultDispatcher-worker-1] test9Odd for x = 108...
    [DefaultDispatcher-worker-1] Is 108 odd? no

My question is: Why isn't the code in the main() function running back on the main thread after the 1st call?

Note that if I remove the withContext in the main function, then it switches back to the main thread. If I replace withContext with launch however, it doesn't change this issue.

CodePudding user response:

I know that coroutine testing has been up until recently described as "flaky" and difficult to set up in a way that gives expected results.

They recently overhauled the test library, and TestCoroutineDispatcher is deprecated.

The following test works for me:

class Tests {

    @Before
    fun before() {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @Test
    fun `switch between dispatchers`() = runTest {
        withContext(Dispatchers.Main) {
            log("test9")
            for (x in listOf(8, 97, 108)) {
                val result = test9Odd(x)
                log("Is $x odd? ${if (result) "yes" else "no"}")
            }
        }
    }

}

Output:

[Test worker @coroutine#1] test9
[DefaultDispatcher-worker-1] test9Odd for x = 8...
[Test worker @coroutine#1] Is 8 odd? no
[DefaultDispatcher-worker-1] test9Odd for x = 97...
[Test worker @coroutine#1] Is 97 odd? yes
[DefaultDispatcher-worker-1] test9Odd for x = 108...
[Test worker @coroutine#1] Is 108 odd? no

This also works:

    fun before() {
        Dispatchers.setMain(newSingleThreadContext("main"))
    }

Running it outside a test like this works, too:

fun main() {
    Dispatchers.setMain(newSingleThreadContext("main"))
    runBlocking {
        withContext(Dispatchers.Main) {
            log("test9")
            for (x in listOf(8, 97, 108)) {
                val result = test9Odd(x)
                log("Is $x odd? ${if (result) "yes" else "no"}")
            }
        }
    }
}
  • Related