Please consider the following scenarios in which I'm using a ThreadLocal as a Kotlin coroutine context element.
Scenario 1 - use withContext()
to set the value of a ThreadLocal on an inner block:
val threadLocal = ThreadLocal<String?>()
fun main() = runBlocking(Dispatchers.IO + threadLocal.asContextElement("main")) {
println("#1: ${threadLocal.get()}")
withContext(coroutineContext + threadLocal.asContextElement("inner")) {
println("#2: ${threadLocal.get()}")
async {
println("#3: ${threadLocal.get()}")
}.await()
println("#4: ${threadLocal.get()}")
}
println("#5: ${threadLocal.get()}")
}
This produces the following output:
#1: main
#2: inner
#3: inner
#4: inner
#5: main
Using the ThreadLocal as a coroutine context element, the value is first set to "main", then changed to "inner" with an inner context block. In particular note that #3 gets the value "inner" as well. In my understanding, this is how coroutine context inheritance is supposed to work. A child coroutine inherits the context from its parent.
Now consider this minor variation on the same setup, in which the call to withContext()
is made from another function:
Scenario 2 - withContext()
called from a function
fun main() = runBlocking(Dispatchers.IO + threadLocal.asContextElement("main")) {
println("#1: ${threadLocal.get()}")
withThreadLocalValue("inner") { // <-- my function wrapping call to 'withContext()'
println("#2: ${threadLocal.get()}")
async {
println("#3: ${threadLocal.get()}")
}.await()
println("#4: ${threadLocal.get()}")
}
println("#5: ${threadLocal.get()}")
}
suspend inline fun <T> withThreadLocalValue(value: String, crossinline block: suspend () -> T):T =
withContext(coroutineContext + threadLocal.asContextElement(value)) {
block()
}
In this case, instead of using withContext()
directly to set the ThreadLocal "inner" context, the same call to withContext()
is made from a separate (inline) function, that then invokes the enclosing block.
This produces the following output:
#1: main
#2: inner
#3: main
#4: inner
#5: main
Note that, as expected, both #2 and #4 get the "inner" ThreadLocal value. #3 though, inside the async
block, does not inherit that "inner" value on its context, but instead has the "main" value from the surrounding context.
So my questions are:
withContext()
from another function?You made a small mistake in your implementation of withThreadLocalValue
. Please note the original withContext
function accepts a block: suspend CoroutineScope.() -> T
. Our lambda receives a new coroutine scope - scope of the inner coroutine, so async
is related to the inner coroutine. withThreadLocalValue
doesn't provide the inner coroutine scope to the lambda, so the lambda still uses the outer scope - async
creates a coroutine in runBlocking
, not in the inner coroutine.
To fix the problem, we just need to provide the inner coroutine scope to the lambda:
suspend inline fun <T> withThreadLocalValue(value: String, crossinline block: suspend CoroutineScope.() -> T):T =
Then it prints inner
for #3
.