ScopedValue structured forking/forwarding API
Nikita Bobko
nikita.bobko at jetbrains.com
Wed Jul 9 15:54:56 UTC 2025
> An initial question for you: Would I correct to say that the
> currentThread may change across suspension points when using these
> coroutines? Just asking as it isn't immediately clear if these
> coroutines are compatible with ThreadLocals and other APIs/features that
> depend on Thread identity.
I understand why you are asking, and yes, currentThread may change
across suspension points. It's not a problem though in Kotlin Coroutines
because we have our ways to carry arbitrary data over threads.
We even have a way to preserve ThreadLocals:
runBlocking(Dispatchers.Default) {
// https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/with-context.html
// https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html
// ThreadLocal set by withContext + asContextElement are preserved
withContext(local.asContextElement("Will be restored")) {
// suspension point
delay(1.seconds)
// Prints "Will be restored"
println(local.get())
}
// Prints "null"
println(local.get())
local.set("Won't be restored")
// suspension point
delay(1.seconds)
// May randomly print "null" or
// "Won't be restored"
println(local.get())
}
It doesn't work perfectly as you can see. The data is carried over only
if it's set in a "managed", structural way that coroutines framework
understands. And that's why we generally advise against using Coroutines
together with ThreadLocals.
Theoretically, we could even preserve all ThreadLocals between
suspension points if there were an API to capture all current
ThreadLocals, which is probably unfeasible from the design point of
view. And anyway, at the moment, I don't think that it's the right API
to have, and I am not asking for it.
> In any case, the summary is that scoped values can only be safely
> inherited into "child threads" in structured contexts. Inheritance is
> one of the privileged operations
I see and respect this point, but it's exactly the point I'm trying to
challenge. I feel that public and safe inheritance API is possible so
that it doesn't have to be the privileged operation.
But I do feel that you understood me, so I just reiterated my point.
> Right now the only API exposed that uses this
> is StructuredTaskScope (STS).
BTW, it's the API we can already use/abuse to get Scoped Values work in
Coroutines:
https://gist.github.com/nikitabobko/e923d187efb7832f1cfb56a66d20b8df
The downside is that users lose the flexibility to choose which threads
they want to run their Coroutines on (e.g. UI main thread is no longer
an option), which feels like an artificial limitation.
And there are multiple existing other ways on how we can
support ScopedValues, each of them having different trade-offs:
https://youtrack.jetbrains.com/issue/KT-78467
(it's just for the reference, I don't expect you to read all of this)
> There has been some experiments with inheriting into "child tasks",
> which I suspect is closer to what you are asking for.
If by "child tasks", you mean
arbitrary-3rd-party-framework-structured-child-tasks, then yes :)
If you mean ForkJoinTask specifically, probably not so much, but it'd be
a good start, I guess.
> interest into inheriting into the tasks supporting the
> stages of a parallel stream pipeline
> These experiments are
> parked right now. I read your mail as looking to reboot these explorations.
Ha, interesting. It didn't cross my mind that ScopedValues make sense in
parallel streams, thank you for sharing.
Anyway, thanks, Alan, for your answer. It's nice to see that there is
also a parallel streams use case, I hope to see if something comes of it
in the future. I am happy to answer whatever questions about the
Coroutines-in-Kotlin-use-case.
- Nikita
More information about the loom-dev
mailing list