ScopedValue structured forking/forwarding API

Robert Engels robaho at me.com
Sun Jul 13 20:34:03 UTC 2025


The whole point of virtual threads is to avoid coroutines / async functions. 

Why would the Java team spend anytime on this?

> On Jul 13, 2025, at 3:31 PM, Nikita Bobko <nikita.bobko at jetbrains.com> wrote:
> 
> Glossary.
> 
> *suspend* functions in Kotlin are code-colored functions. `suspend` is
> keyword that users put on their functions and functional types. All
> suspend functions and lambdas undergo a CPS-transformation performed by
> the Kotlin compiler. In other languages, such functions are commonly
> referred as *async* functions. I may interchangeably call them either
> async or suspend functions.
> 
> Andrew Haley wrote:
>> Is it your intention that a set of ScopedValue bindings be
>> associated with a coroutine, and that ScopedValue bindings be captured
>> at the point at which the coroutine is suspended, and re-bound later
>> when the coroutine is resumed?
> 
> Correct.
> 
> I'd like to take a step back and mention that there are actually two
> possible integrations between ScopedValues and kotlinx.coroutines.
> 
> *Integration 1.* kotlinx.coroutines should provide an utility function
> that would be an analog of ScopedValue.Carrier.run, with the slight
> difference that the function must be suspend, and the lambda that it
> accepts must be also suspend. That's the integration for which we don't
> need anything else on top of what ScopedValues already provides.
> 
> *Integration 2.* Similar to how child threads in StructuredTaskScope
> have access to all ScopedValues of their parent thread, we want to
> forward all ScopedValues from the blocked parent thread (yes, parent
> thread, not parent coroutine, please read further) to children
> coroutines. For that kind of integration, we need something along the
> lines of what I've described at the beginning of the thread.
> 
> In kotlinx.coroutines, we want to provide both integrations.
> 
> Andrew Haley wrote:
>> However, it is straightforward to create a scoped value class of your
>> own that has any properties you wish, including different inheritance
>> rules. Given a ScopedValue.Carrier that binds some values, start your
>> thread with aCarrier.run(task). Would this work for you?
> 
> Andrew Haley wrote:
>> I'm baffled. Your proposal involves invoking bindings.forward() when
>> (re)starting a task:
>> 
>>                         pool.submit(() -> {
>>                             bindings.forward(() -> {
>> 
>> This has to be explicit, and has to be done when there are no bound
>> scoped values. Why can't your implementation do it?
> 
> I think the misunderstanding may be comming from the fact that we were
> thinking about different kinds of integrations.
> 
> For integration 1, we indeed are going to do something along the lines
> of what you describe with ScopedValue.Carrier object.
> 
> To better understand integration 2, I think it is easier to see what I
> mean by looking at Kotlin code (I understand that you may not be
> familiar with Kotlin, I tried to make it as Java as possible):
> 
>    // The lambda below is obviously non-suspend,
>    // we just call a Java API from Kotlin
>    ScopedValue.where(scopedValue, "Duke").run({
> 
>        // Below is a non-suspend lambda with a single CoroutineScope
>        // parameter. I extracted the lambda to a "spawner" variable
>        // because of some Kotlin-unrelated-details
>        val spawner: (CoroutineScope) -> Unit = { scope: CoroutineScope ->
> 
>            // Spawn a child coroutine and schedule to run it on UI main
>            // thread.
>            scope.launch(Dispatchers.Main, block = { // suspend lambda
> 
>                // Right now, it fails with:
>                //     NoSuchElementException: ScopedValue not bound
>                // My goal is make it return "Duke"
>                println(scopedValue.get())
> 
>                // Non blocking suspension point.
>                // delay is a suspend function and can be called only
>                // from other suspend functions
>                delay(1.second)
> 
>                // Right now, it fails with:
>                //     NoSuchElementException: ScopedValue not bound
>                // My goal is make it return "Duke"
>                println(scopedValue.get())
> 
>            })
>        }
> 
>        // runBlocking runs the lambda and keeps the current thread
>        // blocked until all the spawned child coroutines are done.
>        runBlocking(block = spawner)
>    }
> 
> - Kotlin's runBlocking function is identical to Java's
>  StructuredTaskScope.open() function
> - Kotlin's CoroutineScope.launch function is identical to Java's
>  StructuredTaskScope.fork function
> - Kotlin's CoroutineScope is identical to Java's StructuredTaskScope
> - In java, one has to manually do StructuredTaskScope.join(). In Kotlin,
>  we do it implicitly at the end of runBlocking function.
> 
> Coroutines are not magical. Effectively, CPS-transformation slices
> suspend functions by their suspension points. And under layers of
> abstractions, kotlinx.coroutine still submits those slices to whatever
> thread by doing old and boring threadPool.submit().
> 
> Suppose that the API that I'm suggesting at the beginning of the thread
> existed, then at the beginning of runBlocking implementation, I'd
> capture all the current bindings, and stash them inside a CoroutineScope
> instance. To keep everything structural and sound, I obviously call
> ScopedValue.Snapshot.join() and ScopedValue.Snapshot.close() on the
> stashed bindings at the end of the runBlocking function.
> 
> And once we pass all further layers of abstractions, and get to the
> point when we actually need to submit a sliece of a coroutine on a
> threadPool. Instead of calling threadPool.submit(coroutineSlice), we
> would just wrap coroutineSlice into ScopedValue.Snapshot.forward call:
> 
>    threadPool.submit(() -> {
>        coroutineScope.stashedParentScopedValueBindings.forward(() -> {
>            coroutineSlice.run();
>        });
>    });
> 
> so that a child coroutine has access to ScopedValues of its blocked
> parent spawner thread, yay! Task accomplished, Java is once again a
> great platform for integration, Nikita is happy. Mic drop.
> 
> Basically, the difference between these two integrations is that the
> first integration bridges suspend world with the very same suspend world
> with ScopedValue bindings in the middle. But the second integration
> bridges Java's non-suspend world where some of ScopedValues bindings
> already existed with Kotlin's suspend world.


More information about the loom-dev mailing list