Module java.base

Class StructuredTaskScope<T>

java.lang.Object
java.util.concurrent.StructuredTaskScope<T>
Type Parameters:
T - the result type of tasks executed in the task scope
All Implemented Interfaces:
AutoCloseable
Direct Known Subclasses:
StructuredTaskScope.ShutdownOnFailurePREVIEW, StructuredTaskScope.ShutdownOnSuccessPREVIEW

public class StructuredTaskScope<T> extends Object implements AutoCloseable
StructuredTaskScope is a preview API of the Java platform.
Programs can only use StructuredTaskScope when preview features are enabled.
Preview features may be removed in a future release, or upgraded to permanent features of the Java platform.
A basic API for structured concurrency. StructuredTaskScope supports cases where a task splits into several concurrent subtasks, and where the subtasks must complete before the main task continues. A StructuredTaskScope can be used to ensure that the lifetime of a concurrent operation is confined by a syntax block, just like that of a sequential operation in structured programming.

Basic operation

A StructuredTaskScope is created with one of its public constructors. It defines the fork method to start a thread to execute a subtask, the join method to wait for all subtasks to finish, and the close method to close the task scope. The API is intended to be used with the try-with-resources statement. The intention is that code in the try block uses the fork method to fork threads to execute the subtasks, wait for the subtasks to finish with the join method, and then process the results. A call to the fork method returns a SubtaskPREVIEW to representing the forked subtask. Once join is called, the Subtask can be used to get the result completed successfully, or the exception if the subtask failed.
    Callable<String> task1 = ...
    Callable<Integer> task2 = ...

    try (var scope = new StructuredTaskScope<Object>()) {

        Subtask<String> subtask1 = scope.fork(task1);
        Subtask<Integer> subtask2 = scope.fork(task2);

        scope.join();

        ... process results/exceptions ...

    } // close

The following example forks a collection of homogeneous subtasks, waits for all of them to complete with the join method, and uses the Subtask.StatePREVIEW to partition the subtasks into a set of the subtasks that completed successfully and another for the subtasks that failed.

    List<Callable<String>> callables = ...

    try (var scope = new StructuredTaskScope<String>()) {

        List<Subtask<String>> subtasks = callables.stream().map(scope::fork).toList();

        scope.join();

        Map<Boolean, Set<Subtask<String>>> map = subtasks.stream()
                .collect(Collectors.partitioningBy(h -> h.state() == Subtask.State.SUCCESS,
                                                   Collectors.toSet()));

    } // close

To ensure correct usage, the join and close methods may only be invoked by the owner (the thread that opened/created the task scope), and the close method throws an exception after closing if the owner did not invoke the join method after forking.

StructuredTaskScope defines the shutdown method to shut down a task scope without closing it. The shutdown() method cancels all unfinished subtasks by interrupting the threads. It prevents new threads from starting in the task scope. If the owner is waiting in the join method then it will wakeup.

Shutdown is used for short-circuiting and allow subclasses to implement policy that does not require all subtasks to finish.

Subclasses with policies for common cases

Two subclasses of StructuredTaskScope are defined to implement policy for common cases:
  1. ShutdownOnSuccessPREVIEW 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 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 subtasks. It defines methods to get the first result or throw an exception if all subtasks fail.
  2. ShutdownOnFailurePREVIEW captures the exception of the first subtask to fail. Once captured, it shuts down the task scope to interrupt unfinished threads and wakeup the owner. 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.

The following are two examples that use the two classes. In both cases, a pair of subtasks are forked to fetch resources from two URL locations "left" and "right". The first example creates a ShutdownOnSuccess object to capture the result of the first subtask to complete successfully, cancelling the other by way of shutting down the task scope. The main task waits in join until either subtask completes with a result or both subtasks fail. It invokes result(Function)PREVIEW method to get the captured result. If both subtasks fail then this method throws a WebApplicationException with the exception from one of the subtasks as the cause.

    try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {

        scope.fork(() -> fetch(left));
        scope.fork(() -> fetch(right));

        scope.join();

        String result = scope.result(e -> new WebApplicationException(e));

        ...
    }
The second example creates a ShutdownOnFailure object to capture the exception of the first subtask to fail, cancelling the other by way of shutting down the task scope. The main task waits in joinUntil(Instant) until both subtasks complete with a result, either fails, or a deadline is reached. It invokes throwIfFailed(Function)PREVIEW to throw an exception if either subtask fails. This method is a no-op if both subtasks complete successfully. The example uses Supplier.get() to get the result of each subtask. Using Supplier instead of Subtask is preferred for common cases where the object returned by fork is only used to get the result of a subtask that completed successfully.
   Instant deadline = ...

   try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

        Supplier<String> supplier1 = scope.fork(() -> query(left));
        Supplier<String> supplier2 = scope.fork(() -> query(right));

        scope.joinUntil(deadline);

        scope.throwIfFailed(e -> new WebApplicationException(e));

        // both subtasks completed successfully
        String result = Stream.of(supplier1, supplier2)
                .map(Supplier::get)
                .collect(Collectors.joining(", ", "{ ", " }"));

        ...
    }

Extending StructuredTaskScope

StructuredTaskScope can be extended, and the handleComplete method overridden, to implement policies other than those implemented by ShutdownOnSuccess and ShutdownOnFailure. A subclass may, for example, collect the results of subtasks that complete successfully and ignore subtasks that fail. It may collect exceptions when subtasks fail. It may invoke the shutdown method to shut down and cause join to wakeup when some condition arises.

A subclass will typically define methods to make available results, state, or other outcome to code that executes after the join method. A subclass that collects results and ignores subtasks that fail may define a method that returns the results. A subclass that implements a policy to shut down when a subtask fails may define a method to get the exception of the first subtask to fail.

The following is an example of a simple StructuredTaskScope implementation that collects homogenous subtasks that complete successfully. It defines the method "completedSuccessfully()" that the main task can invoke after it joins.

    class CollectingScope<T> extends StructuredTaskScope<T> {
        private final Queue<Subtask<? extends T>> subtasks = new LinkedTransferQueue<>();

        @Override
        protected void handleComplete(Subtask<? extends T> subtask) {
            if (subtask.state() == Subtask.State.SUCCESS) {
                subtasks.add(subtask);
            }
        }

        @Override
        public CollectingScope<T> join() throws InterruptedException {
            super.join();
            return this;
        }

        public Stream<Subtask<? extends T>> completedSuccessfully() {
            super.ensureOwnerAndJoined();
            return subtasks.stream();
        }
    }

The implementations of the completedSuccessfully() method in the example invokes ensureOwnerAndJoined() to ensure that the method can only be invoked by the owner thread and only after it has joined.

Tree structure

Task scopes form a tree where parent-child relations are established implicitly when opening a new task scope:
  • A parent-child relation is established when a thread started in a task scope opens its own task scope. A thread started in task scope "A" that opens task scope "B" establishes a parent-child relation where task scope "A" is the parent of task scope "B".
  • A parent-child relation is established with nesting. If a thread opens task scope "B", then opens task scope "C" (before it closes "B"), then the enclosing task scope "B" is the parent of the nested task scope "C".
The descendants of a task scope are the child task scopes that it is a parent of, plus the descendants of the child task scopes, recursively.

The tree structure supports:

  • Inheritance of scoped valuesPREVIEW across threads.
  • Confinement checks. The phrase "threads contained in the task scope" in method descriptions means threads started in the task scope or descendant scopes.

The following example demonstrates the inheritance of a scoped value. A scoped value USERNAME is bound to the value "duke". A StructuredTaskScope is created and its fork method invoked to start a thread to execute childTask. The thread inherits the scoped value bindings captured when creating the task scope. The code in childTask uses the value of the scoped value and so reads the value "duke".

    private static final ScopedValue<String> USERNAME = ScopedValue.newInstance();

    ScopedValue.runWhere(USERNAME, "duke", () -> {
        try (var scope = new StructuredTaskScope<String>()) {

            scope.fork(() -> childTask());
            ...
         }
    });

    ...

    String childTask() {
        String name = USERNAME.get();   // "duke"
        ...
    }

StructuredTaskScope does not define APIs that exposes the tree structure at this time.

Unless otherwise specified, passing a null argument to a constructor or method in this class will cause a NullPointerException to be thrown.

Memory consistency effects

Actions in the owner thread of, or a thread contained in, the task scope prior to forking of a subtask happen-before any actions taken by that subtask, which in turn happen-before the subtask result is retrievedPREVIEW or happen-before any actions taken in a thread after joining of the task scope.

See Java Language Specification:
17.4.5 Happens-before Order
Since:
21