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