Structured concurrency: TaskHandle

forax at univ-mlv.fr forax at univ-mlv.fr
Fri May 12 18:56:35 UTC 2023


----- Original Message -----
> From: "Ron Pressler" <ron.pressler at oracle.com>
> To: "Remi Forax" <forax at univ-mlv.fr>, "Alan Bateman" <alan.bateman at oracle.com>
> Cc: "loom-dev" <loom-dev at openjdk.org>
> Sent: Friday, May 12, 2023 7:39:25 PM
> Subject: Re: Structured concurrency: TaskHandle

> I think there’s a point here that’s worth an emphasis:
> 
> When join completes what we know is that every fork has either terminated *or*
> deemed irrelevant by a call to shutdown that tells the scope: “I have all the
> results I need to finish the parent tasks (successfully or not) and I don’t
> care about further results”. It is only when close returns that we know that
> all forks have actually terminated.
> 
> I.e.:
> 
> 1. After join: I have all I need to compute a result (but some forks might still
> be running).
> 
> 2. After close: All forks have terminated.
> 
> It is useful not to require that all forks have terminated when join completes
> because join can complete abnormally if interrupted. In that situation, the
> scope can decide whether to shutdown (cancelling all threads) or not. If
> InterruptedException is not explicitly handle (i.e. the “default”) then the
> scope will shut down because the exception will then trigger an implicit close,
> which will shutdown the scope; close is not interruptible.
> 
> — Ron


I was thinking of this peculiar scenario, where the states are read after a call to join()

        try(var scope = new StructuredTaskScope<Double>()) {
            var future1 = scope.fork(() -> {
                var result = 0.5;
                for(var i = 0; i < 10_000_000; i++) {
                    result = Math.sin(result) + result;
                }
                return result;
            });
            var future2 = scope.fork(() -> {
                Thread.sleep(1_000);
                throw new IOException();
            });
            Thread.sleep(1);
            scope.shutdown();
            scope.join();
            System.out.println("future 1: " + future1.state());
            System.out.println("future 2: " + future2.state());
        }

Rémi

> 
> 
>> On 12 May 2023, at 13:14, Alan Bateman <Alan.Bateman at oracle.com> wrote:
>> 
>> On 12/05/2023 16:01, forax at univ-mlv.fr wrote:
>>> :
>>> I would say it in the other way, you need state() to know if the task has
>>> competed successfully then you can call get() or use the handle as Supplier.
>>> Technically, you do not need state() before calling join().
>> 
>> It will take time to get more feedback from real world usages. The examples that
>> we published using ShutdownOnXXX don't need to switch on the state. For
>> ShutdownOnSuccess you can discard the object returned by fork, it's not needed.
>> For ShutdownOnFailure, if join().throwIfFailed() doesn't throw then you are
>> guaranteed that all tasks have completed successfully so you don't need the
>> state either.
>> 
>> 
>>> The non-determinism comes from the way Thread.interrupt() works. If the thread
>>> is interrupted during a blocking call or reach a blocking call an
>>> InterruptedException will be thrown. If there is no blocking call or
>>> Thread.currentThread().interrupt() is called, only the flag is positioned.
>>> I proposed that if the flag is positioned then the state of the task should be
>>> FAILED and if exception() is called, an InterruptException should be thrown
>>> (one with no stacktrace so it can be shared).
>> Calling shutdown means you are done and not interested in the tasks that are
>> still running. Yes, they are interrupted (after the state transition to
>> shutdown so any result/exceptions from the remaining task will be discarded).
>> Are you looking to capture how they reacted to interrupt or what do you mean by
>> "the flag is positioned". Maybe you are thinking about some side effect?
>> 
>>> :
>>> Taking a step back, there are two states, there are the task state and the state
>>> on the object passed to handleComplete.
>>> The former can be RUNNING, SUCCESS, FAILED or CANCELLED, the later is only
>>> SUCCESS or FAILED.
>>> Ìf we have two different objects, each one can have a different enum, TaskState
>>> and ResultState an everything is fine.
>>> If we have use TaskHandle for both, switching on the state inside handleComplete
>>> has two unreachable states but the compiler does not know that.
>>> 
>>> So perhaps the solution is to have two different states.
>> That might be hard to explain so I think we should wait for more real-world
>> usage and feedback before seeing if there are any adjustments required.
>> 
>> 
>>> :
>>> Another question does the API of TaskHandle should be only available to the
>>> owner thread of the scope ?
>> fork can be called by any subtask in the tree. So while of limited value
>> compared to Future in this context, it would surprising if every method threw
>> WTE.
>> 
>> -Alan
>> 
>> 


More information about the loom-dev mailing list