We've run into a case where ThreadContextElement's...
# coroutines
t
We've run into a case where ThreadContextElement's
updateThreadContext
and
restoreThreadContext
don't seem to get invoked if there is no coroutineDispatcher in the context. Ran into this in a small grpc client app. We mistakenly used
suspend fun main
instead of
runBlocking
, so the code was running with no dispatcher. We noticed that our ThreadContextElement was present in the coroutineContext when resumed, but was not being restored to the ThreadLocal. Once we added a coroutineDispatcher to the context, everything worked. Running with a Dispatcher is obviously something useful. But it was a surprise that the coroutineContext was restored without the ThreadLocals. Is this a bug? Reproduced here: https://github.com/taer/coroutine-dispatch-issue
d
I don't think that's a bug.
ThreadContextElement
, like any context element except for
ContinuationInterceptor
, is simply an object lying in a
CoroutineContext
, and someone has to look at it and know to interpret it somehow.
suspend fun main
is implemented on the level of the language, so it clearly has no knowledge of context elements introduced by the
kotlinx.coroutines
library. I don't think it's possible to change this behavior, though it's probably worth explicitly highlighting in the documentation.
t
My learning from this is that the coroutineDispatcher is required for the restoration of the ThreadLocals. My prior assumption was whatever managed the coroutineContext and "delivered" it to the resumed code was responsible for that.
d
coroutineContext
is a language-level feature, as is delivering it to the resumed code, and
ThreadContextElement
is a library-level construct. The language doesn't know about our library.
t
Digging throught the path. The ThreadContextElement.updateThreadContext is called by ThreadContext.updateThreadContext which in turn is called by things in CoroutineContext. All library things still
it starts to get hard to follow past that, since there are a few call-sites.
One is
withContinuationContext
inside CoroutineContext, which sounds semi-generic. But I'm too deep now in code that would take a bit to figure out. 🙂
So is my assertion correct? The Dispatcher is what is responsible for calling
updateThreadContext
s
☝️ yes. Coroutine context is part of the
Continuation
, which is part of the language/stdlib. The injection of library-level features happens when the language calls
continuation.intercepted()
. That calls through to
Copy code
coroutineContext[ContinuationInterceptor]?.interceptContinuation(this)
So by placing a
ContinuationInterceptor
into the coroutine context, the coroutines library can modify (decorate) the continuation (coroutine) that's created by the language. In practice, all the continuation interceptors inherit from
CoroutineDispatcher
. I think it mostly makes sense that the dispatcher deals with thread context, since the dispatcher is the thing providing the thread. I could see an argument for breaking up the
ContinuationInterceptor
class hierarchy to separate out the different responsibility more, though.
(The
withContext
function messes with this pattern to save time, but that's an implementation detail)
t
Thanks!