[External] : Re: Meaning and Usage of Tasks
Eric Kolotyluk
eric at kolotyluk.net
Fri Nov 19 20:34:16 UTC 2021
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>
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> 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>
> 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> 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>
>> 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> 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