Java 21 Structured Concurrency ShutdownOnSuccess and ShutdownOnFailure understanding

164 Views Asked by At

I am trying to understand the structured concurrency (JEP 453) being previewed in current Java 21.

I have this example in which I'm using ShutdownOnSuccess and ShutdownOnFailure.

As mentioned in java docs,

  • ShutdownOnSuccess captures the first result and shuts down the task scope to interrupt unfinished threads and wakeup the owner. This class is intended for cases where the result of any subtask will do ("invoke any") and where there is no need to wait for results of other unfinished tasks. It defines methods to get the first result or throw an exception if all subtasks fail.

  • ShutdownOnFailure captures the first exception and shuts down the task scope. This class is intended for cases where the results of all subtasks are required ("invoke all"); if any subtask fails then the results of other unfinished subtasks are no longer needed. If defines methods to throw an exception if any of the subtasks fail.

what I understand is in ShutdownOnSuccess case if one task get completed rest will be interrupted and in ShutdownOnFailure all task needs to be completed , then shutdown call is made.

So time to complete task assigned in success will be less then failure. is this understanding correct ?

if yes, then time to complete Success is coming greater than failure in every execution, why ?

Here is the sample code.

package com.example.threadex;

import java.time.Duration;
import java.time.Instant;
import java.util.Random;
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;

public class StructuredTaskScopeExample {
    public static void main(String[] args) {
        Instant start = Instant.now();
        try(var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()){
            Subtask<String> task1 = scope.fork(Weather::getTempFromA);
            Subtask<String> task2 = scope.fork(Weather::getTempFromB);
            Subtask<String> task3 = scope.fork(Weather::getTempFromC);
            scope.join();
            System.out.println(STR."""
                    task1: \{task1.state()}: result : \{task1.state() == Subtask.State.SUCCESS ? task1.get() : "Not Available"}
                    task2: \{task2.state()}: result : \{task2.state() == Subtask.State.SUCCESS ? task2.get() : "Not Available"}
                    task3: \{task3.state()}: result : \{task3.state() == Subtask.State.SUCCESS ? task3.get() : "Not Available"}
                    """);

        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
        System.out.println(STR."AnySuccess : \{Duration.between(start, Instant.now()).toMillis()}ms");
        System.out.println("==================");
        Instant start1 = Instant.now();
        try(var scope1 = new StructuredTaskScope.ShutdownOnFailure()){
            Subtask<String> task1 = scope1.fork(Weather::getTempFromA);
            Subtask<String> task2 = scope1.fork(Weather::getTempFromB);
            Subtask<String> task3 = scope1.fork(Weather::getTempFromC);
            scope1.join();
            scope1.throwIfFailed(RuntimeException::new);
            System.out.println(STR."""
                    task1: \{task1.state()}: result: \{task1.get()}
                    task2: \{task2.state()}: result: \{task2.get()}
                    task3: \{task3.state()}: result: \{task3.get()}
                    """);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println(STR."AllSuccess : \{Duration.between(start1, Instant.now()).toMillis()}ms");
    }

    static class Weather{
        static Random random = new Random();
        public static String getTempFromA(){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return STR."Temp from A: Temp = \{random.nextInt(0, 100)}";
        }
        public static String getTempFromB(){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return STR."Temp from B: Temp = \{random.nextInt(0, 100)}";
        }

        public static String getTempFromC(){
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            return STR."Temp from C: Temp = \{random.nextInt(0, 100)}";
        }


    }
}

Output i'm getting:

task1: SUCCESS: result : Temp from A: Temp = 57
task2: SUCCESS: result : Temp from B: Temp = 20
task3: SUCCESS: result : Temp from C: Temp = 45

AnySuccess : 57ms
==================
task1: SUCCESS: result: Temp from A: Temp = 78
task2: SUCCESS: result: Temp from B: Temp = 54
task3: SUCCESS: result: Temp from C: Temp = 59

AllSuccess : 18ms

Can anyone explain why this difference ?

Thanks

3

There are 3 best solutions below

0
Gurkan İlleez On

I didnt work in subtasks but from your description I see that invoke any thread means: just start a task one of them. Invoke All means that start all of them.

So when you think about it one of them is invoking one task and getting first result. Other is invoking all tasks at the same time.

5
DuncG On

Your results show that all 3 tasks are kicked off together for both runs. As these tasks each run for around 10ms they may finish before the "ShutdownOnSuccess" handler can stop the other two.

Run your test in a loop and after warmup you should see similar run times and occasionally some of the task results are UNAVAILABLE, showing the expected behaviour of "ShutdownOnSuccess" handler:

for (int i = 0; i < 30; i++) {
    ... as before    
}

If you want to see ShutdownOnSuccess work in one run, change the sleep times of task1 to 1000, and task2 to 100 so that task1/2 are likely to be unfinished when task3 exits. Then the following run should confirm AnySuccess has cancelled task1/task2 as soon as task3 exits:

task1: UNAVAILABLE: result : Not Available
task2: UNAVAILABLE: result : Not Available
task3: SUCCESS: result : Temp from C: Temp = XXXX
0
Naman On

Your understanding of the ShutdownOnSuccess is nearly correct, but over ShutdownOnFailure is not completely true.

what I understand is in ShutdownOnSuccess case if one task get completed rest will be interrupted and in ShutdownOnFailure all task needs to be completed , then shutdown call is made.

Javadoc for both of these explains the difference and clears out the approach of how they process the subtasks. I further quote(formatted):

ShutdownOnSuccess (invoke any)

... captures the result of the first subtask to complete successfully. Once captured, it shuts down the task scope to interrupt unfinished threads and wakeup the task scope owner.

ShutdownOnFailure (invoke all)

... captures the exception of the first subtask to fail. Once captured, it shuts down the task scope to interrupt unfinished threads and wakeup the task scope owner.


Performance

Of the implementation shared in your question and a comparison performance of both the scope types. You would find that there wouldn't be much difference with a better benchmarking technique. For example, benchmarking the same code with the help of JMH as follows

@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 1, batchSize = 1000)

results with comparable performance

Benchmark                                               Mode  Cnt      Score     Error  Units
ShutdownSuccessVsShutdownOnFailure.onFailure          sample   10  12252.401 ± 228.142  ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.00    sample       11844.714            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.50    sample       12297.699            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.90    sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.95    sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.99    sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.999   sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.9999  sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p1.00    sample       12364.808            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess          sample   10  12316.154 ± 105.906  ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.00    sample       12180.259            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.50    sample       12339.642            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.90    sample       12396.685            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.95    sample       12398.363            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.99    sample       12398.363            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.999   sample       12398.363            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.9999  sample       12398.363            ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p1.00    sample       12398.363            ms/op

The reason for this being comparable is the specified time spent per task(sleep time in your case). If you were to change the sleep time to a higher value for one of the tasks, the difference in the output would be more significant. You could see that with a sleep of 10, 20, and 100 ms in respective tasks.

Benchmark                                               Mode  Cnt      Score      Error  Units
ShutdownSuccessVsShutdownOnFailure.onFailure          sample    5  52559.662 ± 3097.716  ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.00    sample       51942.261             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.50    sample       52143.587             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.90    sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.95    sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.99    sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.999   sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p0.9999  sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onFailure:p1.00    sample       53888.418             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess          sample    5   7202.459 ±   81.076  ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.00    sample        7172.260             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.50    sample        7197.426             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.90    sample        7222.591             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.95    sample        7222.591             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.99    sample        7222.591             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.999   sample        7222.591             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p0.9999  sample        7222.591             ms/op
ShutdownSuccessVsShutdownOnFailure.onSuccess:p1.00    sample        7222.591             ms/op