[External] : Re: Meaning and Usage of Tasks

Ron Pressler ron.pressler at oracle.com
Fri Nov 19 20:38:58 UTC 2021


If you read the structured concurrency JEP draft, you’ll see some sections that are still in progress that address the points you made. Stay tuned.
Please try the new hierarchical JEP draft to see how some of those new APIs, which are still undocumented in the JEP, are used.

— Ron

On 19 Nov 2021, at 20:34, Eric Kolotyluk <eric at kolotyluk.net<mailto:eric at kolotyluk.net>> wrote:

For

try (var structuredExecutor = StructuredExecutor.open("Experiment00", virtualThreadFactory)) {
    var completionHandler = new StructuredExecutor.ShutdownOnFailure();
    var futureResults = IntStream.range(0, 15).mapToObj(item -> {
        System.out.printf("item = %d, Thread ID = %s\n", item, Thread.currentThread());
        return structuredExecutor.fork(() -> {
            System.out.printf("\ttask = %d, Thread ID = %s\n", item, Thread.currentThread());
            return item;
        }, completionHandler);
    });
}

a satisfying experience in the future for me would be to see something like

task = 0, Thread ID = Experiment00/VirtualThread[#15]@CarrierThread[#5]
task = 1, Thread ID = Experiment00/VirtualThread[#22]@CarrierThread[#6]
task = 2, Thread ID = Experiment00/VirtualThread[#28]@CarrierThread[#7]
task = 3, Thread ID = Experiment00/VirtualThread[#30]@CarrierThread[#7]

Some useful variations might be

task = a, Thread ID = .../Experiment00/VirtualThread[#15]@CarrierThread[#5]
task = b, Thread ID = .../Experiment00/VirtualThread[#22]@CarrierThread[#6]
task = c, Thread ID = .../Experiment00/VirtualThread[#28]@CarrierThread[#7]
task = d, Thread ID = .../Experiment00/VirtualThread[#30]@CarrierThread[#7]

indicating that these threads have a parent, that can be observed through some other API, such as Thread.getParent()

task = a, Thread ID = Experiment00/VirtualThread[#15]/... at CarrierThread[#5]
task = b, Thread ID = Experiment00/VirtualThread[#22]/... at CarrierThread[#6]
task = c, Thread ID = Experiment00/VirtualThread[#28]/... at CarrierThread[#7]
task = d, Thread ID = Experiment00/VirtualThread[#30]/... at CarrierThread[#7]

indicating that these threads have children, that can be observed through some other API, such as Thread.getChildren()

While I initially imagined a fuller thread path, that could quickly produce unreadable signatures for .toString(), so the above is something compact but useful in terms of understanding the structure of Structured Concurrency at a glance.

Of course, other people might have better ideas of a good thread signature for .toString().

Cheers, Eric

On Fri, Nov 19, 2021 at 10:31 AM Ron Pressler <ron.pressler at oracle.com<mailto:ron.pressler at oracle.com>> wrote:
It means that at the time toString was called, each of those two threads were on that carrier (obviously, not at the same time). I’m not sure how helpful this is, and there’s a good chance we’ll remove that string.

— Ron

On 19 Nov 2021, at 15:48, Eric Kolotyluk <eric at kolotyluk.net<mailto:eric at kolotyluk.net>> wrote:

" The toString method on a virtual thread will print the underlying ForkJoinPool worker, but that’s not a new thread."

Okay, that seems really profound. In my experiment, when I .fork() a number of tasks, and each prints out Thread.currentThread(), I get something like

task = 0, Thread ID = VirtualThread[#15]/runnable at ForkJoinPool-1-worker-1
task = 1, Thread ID = VirtualThread[#22]/runnable at ForkJoinPool-1-worker-6
task = 2, Thread ID = VirtualThread[#28]/runnable at ForkJoinPool-1-worker-11
task = 3, Thread ID = VirtualThread[#30]/runnable at ForkJoinPool-1-worker-11

What does this mean precisely?

For the first one, I took this to mean VirtualThread[#15] is running in a Carrier Thread called runnable at ForkJoinPool-1-worker-1

For VirtualThread[#28] and VirtualThread[#30], these both seem to be running in the same Carrier Thread runnable at ForkJoinPool-1-worker-1

Am I interpreting this wrong? How should I interpret it?

Cheers, Eric


On Fri, Nov 19, 2021 at 5:14 AM Ron Pressler <ron.pressler at oracle.com<mailto:ron.pressler at oracle.com>> wrote:
It will create a new thread depending on the ThreadFactory. If the factory creates virtual threads, it will spawn a virtual thread per task; if it’s platform — platform. The toString method on a virtual thread will print the underlying ForkJoinPool worker, but that’s not a new thread.

On 18 Nov 2021, at 21:44, Eric Kolotyluk <eric at kolotyluk.net<mailto:eric at kolotyluk.net>> wrote:

So, in the case of

var platformThreadFactory = Thread.ofPlatform().factory();

var structuredExecutor = StructuredExecutor.open("Experiment00", platformThreadFactory))

You say "Because StructuredExecutor spawns a new thread for each forked task," it also creates a Platform Thread per task? Is this a property of .fork() or of StructuredExecutor?

When I print out the Thread signature I see

VirtualThread[#36]/runnable at ForkJoinPool-1-worker-11

Which I find confusing, and I hope is improved in the future to be clearer in the relationship between threads and tasks.

No, not an inconvenience at the moment, but I just want to understand better so that I can explain it to others, or ask better questions.

Cheers, Eric


On Thu, Nov 18, 2021 at 1:06 PM Ron Pressler <ron.pressler at oracle.com<mailto:ron.pressler at oracle.com>> wrote:
You bring up multiple points, so let me try and address them.

A task is, indeed, not a thread, and, as you say, I would simply consider it to be some computation, usually expressible as a lambda expression/method reference. Because StructuredExecutor spawns a new thread for each forked task, we might sometimes use the terms interchangeably, but only in this context.

As to the method names and signatures, the execute method is there simply so that StructuredExecutor could implement the Executor interface that declares it. You’ll find something similar in ExecutorService, where there’s a method called executor, that takes a Runnable and returns void, and methods called submit, which take a Callable and return a Future. StructuredExecutor.fork is analogous to ExecutorService.submit. Now, ExecutorService also has an override of submit that takes a Runnable (and returns a Future), in addition to the void execute method. We could certainly add a similar override of fork, but it didn’t seem particularly urgent at this point in time. You can always turn a Runnable into a Callable with the Executors.callable [1] helper method. If you find this to be too much of an inconvenience, please let us know.

— Ron

[1]: https://download.java.net/java/early_access/loom/docs/api/java.base/java/util/concurrent/Executors.html#callable(java.lang.Runnable)<https://urldefense.com/v3/__https://download.java.net/java/early_access/loom/docs/api/java.base/java/util/concurrent/Executors.html*callable(java.lang.Runnable)__;Iw!!ACWV5N9M2RV99hQ!YbndcNiuJTwahpzmlh8-yXOmcGk0pp-R9VHjQpyMByNdAMV83lJfX4WrSLUjhBHNJQ$>

> On 18 Nov 2021, at 20:18, Eric Kolotyluk <eric at kolotyluk.net<mailto:eric at kolotyluk.net>> wrote:
>
> Reading some of the documentation I am getting confused about the meaning
> of a 'Task' and wrote about this earlier in Threads vs Tasks...
>
> Initially, these were just Runnable, and then could also be Callable (which
> is better). Is it interesting to note in StructuredExecutor there are
> different signatures for
>
> .execute(Runnable)
> .fork(Callable)
>
> And I appreciate the distinction because calling .submit(Runnable |
> Callable) can blur things, but can also see that .submit() may be more
> attractive to some. Also, Callable can throw a Checked Exception, whereas
> Runnable cannot. Can someone please explain the reason for this design
> change?
>
> I especially appreciate that both Runnable and Callable can be expressed
> with Lambda expressions, especially coming from a background of Akka/Scala,
> where Actors .spawn() other Actors, and can use Lambda expressions.
> However, in Akka Typed we often use forms such as
>
> object GreeterMain {
>
>  final case class SayHello(name: String)
>
>  def apply(): Behavior[SayHello] =
>    Behaviors.setup { context =>
>      val greeter = context.spawn(Greeter(), "greeter")
>
>      Behaviors.receiveMessage { message =>
>        val replyTo = context.spawn(GreeterBot(max = 3), message.name<https://urldefense.com/v3/__http://message.name__;!!ACWV5N9M2RV99hQ!YbndcNiuJTwahpzmlh8-yXOmcGk0pp-R9VHjQpyMByNdAMV83lJfX4WrSLUHGEwEqw$>)
>        greeter ! Greeter.Greet(message.name<https://urldefense.com/v3/__http://message.name__;!!ACWV5N9M2RV99hQ!YbndcNiuJTwahpzmlh8-yXOmcGk0pp-R9VHjQpyMByNdAMV83lJfX4WrSLUHGEwEqw$>, replyTo)
>        Behaviors.same
>      }
>    }}
>
> where the Behaviors.setup { context => ... is a Lambda that takes a
> 'context'
>
> Now I am not advocating adopting the Akka style, but it makes me wonder
> what it would look like if we had capabilities like
>
> structuredExecutor.spawn( () => {task implementation} )
> structuredExecutor.spawn( context => {task implementation} )
> structuredExecutor.spawn( (context, thingy) => {task implementation} )
> . . .
>
> and so on, where context is something not available in the session because
> the structuredExecutor is available to the task implementation. I am just
> thinking out loud here, where not all Tasks need the context, thingy, etc.,
> but some may benefit from them. More importantly, is there some concept of
> Task that goes beyond Callable, that cannot be satisfied by adding more
> features to Callable?
>
> I am pretty sure the Loom Architects have already walked down this path so
> I would like to know if they could share their thoughts on this.
>
> I am not hung up on a single .spawn() for multiple task types, but I like
> the name because it brings a sense of 'family' to concurrent programming. ��
>
> In my mind, a Task is *not *a Thread, it is something else that can be
> executed on a Thread, but it should always be implementable by a Lambda.
>
> Cheers, Eric






More information about the loom-dev mailing list