Home > database >  Keep the coroutine context when a block under withContext calls another suspend block
Keep the coroutine context when a block under withContext calls another suspend block

Time:05-18

When using withContext in Kotlin, how to keep the new context after calling another suspend function?

For example: https://pl.kotl.in/fkrnEkYZP

fun main() = runBlocking {
    val anotherBlock = suspend {
        assertSame(Dispatchers.IO, coroutineContext[CoroutineDispatcher], "current CoroutineDispatcher should be still Dispatchers.IO")
        // Exception in thread "main" java.lang.AssertionError: current CoroutineDispatcher should be still Dispatchers.IO. Expected <Dispatchers.IO>, actual <BlockingEventLoop@6863cde3> is not same.
    }
    withContext(Dispatchers.IO) {
        assertSame(Dispatchers.IO, coroutineContext[CoroutineDispatcher], "current CoroutineDispatcher should be Dispatchers.IO")
        anotherBlock()
    }
}

Why is the coroutineContext in anotherBlock different with the parent one?

CodePudding user response:

coroutineContext is a property of CoroutineScope. Since your anotherBlock suspend function does not have a CoroutineScope receiver, you are calling coroutineContext on the scope of the runBlocking lambda, so it's getting the runBlocking's dispatcher.

Change your declaration of the function to use extension function syntax instead of a lambda so it can have its own CoroutineScope.

suspend fun CoroutineScope.anotherBlock() {
    assertSame(Dispatchers.IO, coroutineContext[CoroutineDispatcher], "current CoroutineDispatcher should be still Dispatchers.IO")
    // Exception in thread "main" java.lang.AssertionError: current CoroutineDispatcher should be still Dispatchers.IO. Expected <Dispatchers.IO>, actual <BlockingEventLoop@6863cde3> is not same.
}

CodePudding user response:

The problem is that you're not looking at the actual current coroutine context when doing the assert in the suspend lambda, you're in fact capturing the coroutineContext from runBlocking's CoroutineScope.

The reason why it works in withContext directly is because withContext provides a CoroutineScope as this - which shadows the one given by runBlocking, so luckily you get the correct one.

To avoid this pitfall (which occurs regularly when using both suspend functions and CoroutineScope), you should use currentCoroutineContext() when you want to know the coroutine context in which the current code is executing, which is your case here.

If you do this, you'll see the dispatcher is properly inherited:

fun main() = runBlocking {
    val anotherBlock = suspend {
        assertSame(Dispatchers.IO, currentCoroutineContext()[CoroutineDispatcher], "current CoroutineDispatcher should be still Dispatchers.IO in suspend lambda")
    }
    withContext(Dispatchers.IO) {
        assertSame(Dispatchers.IO, coroutineContext[CoroutineDispatcher], "current CoroutineDispatcher should be Dispatchers.IO")
        anotherBlock()
    }
}
  • Related