Interface StructuredTaskScope<T,R>
- Type Parameters:
T
- the result type of subtasks executed in the scopeR
- the result type of the scope
- All Superinterfaces:
AutoCloseable
StructuredTaskScope
is a preview API of the Java platform.
StructuredTaskScope
supports cases
where execution of a task (a unit of work) splits into several concurrent
subtasks, and where the subtasks must complete before the task continues. A
StructuredTaskScope
can be used to ensure that the lifetime of a concurrent operation
is confined by a syntax block, similar to that of a sequential operation in
structured programming.
StructuredTaskScope
defines the static method open
to open
a new StructuredTaskScope
and the close
method to close it.
The API is designed to be used with the try
-with-resources statement where
the StructuredTaskScope
is opened as a resource and then closed automatically.
The code inside the block uses the fork
method to fork subtasks.
After forking, it uses the join
method to wait for all subtasks to
finish (or some other outcome) as a single operation. Forking a subtask starts a new
Thread
to run the subtask. The thread executing the task does not continue
beyond the close
method until all threads started to execute subtasks have finished.
To ensure correct usage, the fork
, join
and close
methods may
only be invoked by the owner thread (the thread that opened the
StructuredTaskScope
), the fork
method may not be called after join
,
the join
method may only be invoked once, and the close
method throws
an exception after closing if the owner did not invoke the join
method after
forking subtasks.
As a first example, consider a task that splits into two subtasks to concurrently
fetch resources from two URL locations "left" and "right". Both subtasks may complete
successfully, one subtask may succeed and the other may fail, or both subtasks may
fail. The task in this example is interested in the successful result from both
subtasks. It waits in the join
method for both subtasks to complete
successfully or for either subtask to fail.
try (var scope = StructuredTaskScope.open
()) {
Subtask<String> subtask1 = scope.fork
(() -> query(left));
Subtask<Integer> subtask2 = scope.fork(() -> query(right));
// throws if either subtask fails
scope.join
();
// both subtasks completed successfully
return new MyResult(subtask1.get
(), subtask2.get
());
} // close
If both subtasks complete successfully then the join
method completes
normally and the task uses the Subtask.get()
PREVIEW method to get
the result of each subtask. If one of the subtasks fails then the other subtask is
cancelled (this will interrupt the thread executing the
other subtask) and the join
method throws StructuredTaskScope.FailedException
PREVIEW with the
exception from the failed subtask as the cause.
To allow for cancellation, subtasks must be coded so that they finish as soon as
possible when interrupted. Subtasks that do not respond to interrupt, e.g. block on
methods that are not interruptible, may delay the closing of a scope indefinitely. The
close
method always waits for threads executing subtasks to finish,
even if the scope is cancelled, so execution cannot continue beyond the close
method until the interrupted threads finish.
In the example, the subtasks produce results of different types (String
and
Integer
). In other cases the subtasks may all produce results of the same type.
If the example had used StructuredTaskScope.<String>open()
then it could
only be used to fork subtasks that return a String
result.
Joiners
In the example above, the task fails if any subtask fails. If all subtasks
succeed then the join
method completes normally. Other policy and outcome is
supported by creating a StructuredTaskScope
with a StructuredTaskScope.Joiner
PREVIEW that
implements the desired policy. A Joiner
handles subtask completion and produces
the outcome for the join
method. In the example above, join
returns null
. Depending on the Joiner
, join
may return a
result, a stream of elements, or some other object. The Joiner
interface defines
factory methods to create Joiner
s for some common cases.
A Joiner
may cancel the scope (sometimes called
"short-circuiting") when some condition is reached that does not require the result of
subtasks that are still executing. Cancelling the scope prevents new threads from being
started to execute further subtasks, interrupts the
threads executing subtasks that have not completed, and causes the join
method
to wakeup with the outcome (result or exception). In the above example, the outcome is
that join
completes with a result of null
when all subtasks succeed.
The scope is cancelled if any of the subtasks fail and join
throws
FailedException
with the exception from the failed subtask as the cause. Other
Joiner
implementations may cancel the scope for other reasons.
Now consider another example that splits into two subtasks. In this example,
each subtask produces a String
result and the task is only interested in
the result from the first subtask to complete successfully. The example uses Joiner.anySuccessfulResultOrThrow()
PREVIEW to
create a Joiner
that makes available the result of the first subtask to
complete successfully. The type parameter in the example is "String
" so that
only subtasks that return a String
can be forked.
try (var scope = StructuredTaskScope.open
(Joiner.<String>anySuccessfulResultOrThrow())) {
scope.fork(callable1);
scope.fork(callable2);
// throws if both subtasks fail
String firstResult = scope.join();
}
In the example, the task forks the two subtasks, then waits in the
join
method for either subtask to complete successfully or for both subtasks to fail.
If one of the subtasks completes successfully then the Joiner
causes the other
subtask to be cancelled (this will interrupt the thread executing the subtask), and
the join
method returns the result from the successful subtask. Cancelling the
other subtask avoids the task waiting for a result that it doesn't care about. If
both subtasks fail then the join
method throws FailedException
with the
exception from one of the subtasks as the cause.
Whether code uses the Subtask
returned from fork
will depend on
the Joiner
and usage. Some Joiner
implementations are suited to subtasks
that return results of the same type and where the join
method returns a result
for the task to use. Code that forks subtasks that return results of different
types, and uses a Joiner
such as Joiner.awaitAllSuccessfulOrThrow()
that
does not return a result, will use Subtask.get()
PREVIEW after joining.
Exception handling
A StructuredTaskScope
is opened with a Joiner
PREVIEW that
handles subtask completion and produces the outcome for the join
method.
In some cases, the outcome will be a result, in other cases it will be an exception.
If the outcome is an exception then the join
method throws StructuredTaskScope.FailedException
PREVIEW with the exception as the cause. For many Joiner
implementations, the exception will be an exception
thrown by a subtask that failed. In the case of allSuccessfulOrThrow
PREVIEW and awaitAllSuccessfulOrThrow
PREVIEW
for example, the exception is from the first subtask to fail.
Many of the details for how exceptions are handled will depend on usage. In some
cases it may be useful to add a catch
block to the try
-with-resources
statement to catch FailedException
. The exception handling may use
instanceof
with pattern matching to handle specific causes.
try (var scope = StructuredTaskScope.open()) {
..
} catch (StructuredTaskScope.FailedException e) {
Throwable cause = e.getCause();
switch (cause) {
case IOException ioe -> ..
default -> ..
}
}
FailedException
but instead leave
it to propagate to the configured uncaught
exception handler for logging purposes.
For cases where a specific exception triggers the use of a default result then it may be more appropriate to handle this in the subtask itself rather than the subtask failing and the scope owner handling the exception.
Configuration
AStructuredTaskScope
is opened with configuration
that consists of a ThreadFactory
to create threads, an optional name for
monitoring and management purposes, and an optional timeout.
The open()
and open(Joiner)
methods create a StructuredTaskScope
with the default configuration. The default
configuration has a ThreadFactory
that creates unnamed
virtual threads,
is unnamed for monitoring and management purposes, and has no timeout.
The 2-arg open
method can be used to create a
StructuredTaskScope
that uses a different ThreadFactory
, has a name for
the purposes of monitoring and management, or has a timeout that cancels the scope if
the timeout expires before or while waiting for subtasks to complete. The open
method is called with a function that is applied to the default
configuration and returns a Configuration
PREVIEW for the
StructuredTaskScope
under construction.
The following example opens a new StructuredTaskScope
with a
ThreadFactory
that creates virtual threads named
"duke-0", "duke-1" ...
ThreadFactory factory = Thread.ofVirtual().name
("duke-", 0).factory();
try (var scope = StructuredTaskScope.open(joiner, cf -> cf.withThreadFactory
(factory))) {
scope.fork( .. ); // runs in a virtual thread with name "duke-0"
scope.fork( .. ); // runs in a virtual thread with name "duke-1"
scope.join();
}
A second example sets a timeout, represented by a Duration
. The timeout
starts when the new scope is opened. If the timeout expires before the join
method has completed then the scope is cancelled. This
interrupts the threads executing the two subtasks and causes the join
method to throw StructuredTaskScope.TimeoutException
PREVIEW.
Duration timeout = Duration.ofSeconds(10);
try (var scope = StructuredTaskScope.open(Joiner.<String>allSuccessfulOrThrow
(),
cf -> cf.withTimeout
(timeout))) {
scope.fork(callable1);
scope.fork(callable2);
List<String> result = scope.join()
.map(Subtask::get)
.toList();
}
Inheritance of scoped value bindings
ScopedValue
PREVIEW supports the execution of a method with a ScopedValue
bound
to a value for the bounded period of execution of the method by the current thread.
It allows a value to be safely and efficiently shared to methods without using method
parameters.
When used in conjunction with a StructuredTaskScope
, a ScopedValue
can also safely and efficiently share a value to methods executed by subtasks forked
in the scope. When a ScopedValue
object is bound to a value in the thread
executing the task then that binding is inherited by the threads created to
execute the subtasks. The thread executing the task does not continue beyond the
close
method until all threads executing the subtasks have finished.
This ensures that the ScopedValue
is not reverted to being unboundPREVIEW (or its previous value) while subtasks are executing.
In addition to providing a safe and efficient means to inherit a value into subtasks,
the inheritance allows sequential code using ScopedValue
be refactored to use
structured concurrency.
To ensure correctness, opening a new StructuredTaskScope
captures the
current thread's scoped value bindings. These are the scoped values bindings that are
inherited by the threads created to execute subtasks in the scope. Forking a
subtask checks that the bindings in effect at the time that the subtask is forked
match the bindings when the StructuredTaskScope
was created. This check ensures
that a subtask does not inherit a binding that is reverted in the task before the
subtask has completed.
A ScopedValue
that is shared across threads requires that the value be an
immutable object or for all access to the value to be appropriately synchronized.
The following example demonstrates the inheritance of scoped value bindings. The
scoped value USERNAME is bound to the value "duke" for the bounded period of a lambda
expression by the thread executing it. The code in the block opens a
StructuredTaskScope
and forks two subtasks, it then waits in the join
method
and aggregates the results from both subtasks. If code executed by the threads
running subtask1 and subtask2 uses ScopedValue.get()
PREVIEW, to get the value of
USERNAME, then value "duke" will be returned.
private static final ScopedValue<String> USERNAME = ScopedValue.newInstance
();
MyResult result = ScopedValue.where
(USERNAME, "duke").call(() -> {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> subtask1 = scope.fork( .. ); // inherits binding
Subtask<Integer> subtask2 = scope.fork( .. ); // inherits binding
scope.join();
return new MyResult(subtask1.get(), subtask2.get());
}
});
A scoped value inherited into a subtask may be
rebound to a new
value in the subtask for the bounded execution of some method executed in the subtask.
When the method completes, the value of the ScopedValue
reverts to its previous
value, the value inherited from the thread executing the task.
A subtask may execute code that itself opens a new StructuredTaskScope
.
A task executing in thread T1 opens a StructuredTaskScope
and forks a
subtask that runs in thread T2. The scoped value bindings captured when T1 opens the
scope are inherited into T2. The subtask (in thread T2) executes code that opens a
new StructuredTaskScope
and forks a subtask that runs in thread T3. The scoped
value bindings captured when T2 opens the scope are inherited into T3. These
include (or may be the same) as the bindings that were inherited from T1. In effect,
scoped values are inherited into a tree of subtasks, not just one level of subtask.
Memory consistency effects
Actions in the owner thread of a StructuredTaskScope
prior to
forking of a subtask
happen-before any actions taken by that subtask, which in turn
happen-before the subtask result is retrievedPREVIEW.
General exceptions
Unless otherwise specified, passing a null
argument to a method in this
class will cause a NullPointerException
to be thrown.
- See Java Language Specification:
-
17.4.5 Happens-before Order
- Since:
- 21
-
Nested Class Summary
Nested ClassesModifier and TypeInterfaceDescriptionstatic interface
Preview.Represents the configuration for aStructuredTaskScope
.static final class
Preview.Exception thrown byjoin()
when the outcome is an exception rather than a result.static interface
Preview.An object used with aStructuredTaskScope
PREVIEW to handle subtask completion and produce the result for the scope owner waiting in thejoin
method for subtasks to complete.static interface
Preview.Represents a subtask forked withfork(Callable)
orfork(Runnable)
.static final class
Preview.Exception thrown byjoin()
if the scope was created with a timeout and the timeout expired before or while waiting injoin
. -
Method Summary
Modifier and TypeMethodDescriptionvoid
close()
Closes this scope.<U extends T>
StructuredTaskScope.SubtaskPREVIEW<U> Fork a subtask by starting a new thread in this scope to execute a method that does not return a result.<U extends T>
StructuredTaskScope.SubtaskPREVIEW<U> Fork a subtask by starting a new thread in this scope to execute a value-returning method.boolean
join()
Returns the result, or throws, after waiting for all subtasks to complete or the scope to be cancelled.static <T> StructuredTaskScopePREVIEW
<T, Void> open()
Opens a newStructuredTaskScope
that can be used to fork subtasks that return results of any type.static <T,
R> StructuredTaskScopePREVIEW <T, R> open
(StructuredTaskScope.JoinerPREVIEW<? super T, ? extends R> joiner) Opens a newStructuredTaskScope
to use the givenJoiner
object.static <T,
R> StructuredTaskScopePREVIEW <T, R> open
(StructuredTaskScope.JoinerPREVIEW<? super T, ? extends R> joiner, Function<StructuredTaskScope.ConfigurationPREVIEW, StructuredTaskScope.ConfigurationPREVIEW> configFunction) Opens a newStructuredTaskScope
to use the givenJoiner
object and with configuration that is the result of applying the given function to the default configuration.
-
Method Details
-
open
static <T,R> StructuredTaskScopePREVIEW<T,R> open(StructuredTaskScope.JoinerPREVIEW<? super T, ? extends R> joiner, Function<StructuredTaskScope.ConfigurationPREVIEW, StructuredTaskScope.ConfigurationPREVIEW> configFunction) Opens a newStructuredTaskScope
to use the givenJoiner
object and with configuration that is the result of applying the given function to the default configuration.The
configFunction
is called with the default configuration and returns the configuration for the new scope. The function may, for example, set the ThreadFactoryPREVIEW or set a timeoutPREVIEW. If the function completes with an exception or error then it is propagated by this method. If the function returnsnull
thenNullPointerException
is thrown.If a
ThreadFactory
is set then itsnewThread
method will be called to create threads when forking subtasks in this scope. If aThreadFactory
is not set then forking subtasks will create an unnamed virtual thread for each subtask.If a timeoutPREVIEW is set then it starts when the scope is opened. If the timeout expires before the scope has joined then the scope is cancelled and the
join
method throwsStructuredTaskScope.TimeoutException
PREVIEW.The new scope is owned by the current thread. Only code executing in this thread can fork, join, or close the scope.
Construction captures the current thread's scoped value bindings for inheritance by threads started in the scope.
- Type Parameters:
T
- the result type of subtasks executed in the scopeR
- the result type of the scope- Parameters:
joiner
- the joinerconfigFunction
- a function to produce the configuration- Returns:
- a new scope
- Since:
- 25
-
open
static <T,R> StructuredTaskScopePREVIEW<T,R> open(StructuredTaskScope.JoinerPREVIEW<? super T, ? extends R> joiner) Opens a newStructuredTaskScope
to use the givenJoiner
object. The scope is created with the default configuration. The default configuration has aThreadFactory
that creates unnamed virtual threads, is unnamed for monitoring and management purposes, and has no timeout.- Implementation Requirements:
- This factory method is equivalent to invoking the 2-arg open method with the given joiner and the identity function.
- Type Parameters:
T
- the result type of subtasks executed in the scopeR
- the result type of the scope- Parameters:
joiner
- the joiner- Returns:
- a new scope
- Since:
- 25
-
open
Opens a newStructuredTaskScope
that can be used to fork subtasks that return results of any type. The scope'sjoin()
method waits for all subtasks to succeed or any subtask to fail.The
join
method returnsnull
if all subtasks complete successfully. It throwsStructuredTaskScope.FailedException
PREVIEW if any subtask fails, with the exception from the first subtask to fail as the cause.The scope is created with the default configuration. The default configuration has a
ThreadFactory
that creates unnamed virtual threads, is unnamed for monitoring and management purposes, and has no timeout.- Implementation Requirements:
- This factory method is equivalent to invoking the 2-arg open method with a joiner
created with
awaitAllSuccessfulOrThrow()
PREVIEW and the identity function. - Type Parameters:
T
- the result type of subtasks- Returns:
- a new scope
- Since:
- 25
-
fork
Fork a subtask by starting a new thread in this scope to execute a value-returning method. The new thread executes the subtask concurrently with the current thread. The parameter to this method is aCallable
, the new thread executes itscall()
method.This method first creates a
Subtask
PREVIEW object to represent the forked subtask. It invokes the joiner'sonFork
PREVIEW method with the subtask in theUNAVAILABLE
PREVIEW state. If theonFork
completes with an exception or error then it is propagated by thefork
method without creating a thread. If the scope is already cancelled, oronFork
returnstrue
to cancel the scope, then this method returns theSubtask
, in theUNAVAILABLE
PREVIEW state, without creating a thread to execute the subtask.If the scope is not cancelled, and the
onFork
method returnsfalse
, then a thread is created with theThreadFactory
configured when the scope was opened, and the thread is started. Forking a subtask inherits the current thread's scoped value bindings. The bindings must match the bindings captured when the scope was opened. If the subtask completes (successfully or with an exception) before the scope is cancelled, then the thread invokes the joiner'sonComplete
PREVIEW method with the subtask in theSUCCESS
PREVIEW orFAILED
PREVIEW state. If theonComplete
method completes with an exception or error, then the uncaught exception handler is invoked with the exception or error before the thread terminates.This method returns the
Subtask
PREVIEW object. In some usages, this object may be used to get its result. In other cases it may be used for correlation or be discarded. To ensure correct usage, theSubtask.get()
PREVIEW method may only be called by the scope owner to get the result after it has waited for subtasks to complete with thejoin
method and the subtask completed successfully. Similarly, theSubtask.exception()
PREVIEW method may only be called by the scope owner after it has joined and the subtask failed. If the scope was cancelled before the subtask was forked, or before it completes, then neither method can be used to obtain the outcome.This method may only be invoked by the scope owner.
- Type Parameters:
U
- the result type- Parameters:
task
- the value-returning task for the thread to execute- Returns:
- the subtask
- Throws:
WrongThreadException
- if the current thread is not the scope ownerIllegalStateException
- if the owner has already joined or the scope is closedStructureViolationExceptionPREVIEW
- if the current scoped value bindings are not the same as when the scope was createdRejectedExecutionException
- if the thread factory rejected creating a thread to run the subtask
-
fork
Fork a subtask by starting a new thread in this scope to execute a method that does not return a result.This method works exactly the same as
fork(Callable)
except that the parameter to this method is aRunnable
, the new thread executes itsrun
method, andSubtask.get()
PREVIEW returnsnull
if the subtask completes successfully.- Type Parameters:
U
- the result type- Parameters:
task
- the task for the thread to execute- Returns:
- the subtask
- Throws:
WrongThreadException
- if the current thread is not the scope ownerIllegalStateException
- if the owner has already joined or the scope is closedStructureViolationExceptionPREVIEW
- if the current scoped value bindings are not the same as when the scope was createdRejectedExecutionException
- if the thread factory rejected creating a thread to run the subtask- Since:
- 25
-
join
Returns the result, or throws, after waiting for all subtasks to complete or the scope to be cancelled.This method waits for all subtasks started in this scope to complete or the scope to be cancelled. If a timeoutPREVIEW is configured and the timeout expires before or while waiting, then the scope is cancelled and
TimeoutException
PREVIEW is thrown. Once finished waiting, theJoiner
'sresult()
PREVIEW method is invoked to get the result or throw an exception. If theresult()
method throws then this method throwsFailedException
with the exception as the cause.This method may only be invoked by the scope owner, and only once.
- Returns:
- the result
- Throws:
WrongThreadException
- if the current thread is not the scope ownerIllegalStateException
- if already joined or this scope is closedStructuredTaskScope.FailedExceptionPREVIEW
- if the outcome is an exception, thrown with the exception fromJoiner.result()
PREVIEW as the causeStructuredTaskScope.TimeoutExceptionPREVIEW
- if a timeout is set and the timeout expires before or while waitingInterruptedException
- if interrupted while waiting- Since:
- 25
-
isCancelled
boolean isCancelled()Returnstrue
if this scope is cancelled or in the process of being cancelled, otherwisefalse
.Cancelling the scope prevents new threads from starting in the scope and interrupts threads executing unfinished subtasks. It may take some time before the interrupted threads finish execution; this method may return
true
before all threads have been interrupted or before all threads have finished.- API Note:
- A task with a lengthy "forking phase" (the code that executes before
it invokes
join
) may use this method to avoid doing work in cases where scope is cancelled by the completion of a previously forked subtask or timeout. - Returns:
true
if this scope is cancelled or in the process of being cancelled, otherwisefalse
- Since:
- 25
-
close
void close()Closes this scope.This method first cancels the scope, if not already cancelled. This interrupts the threads executing unfinished subtasks. This method then waits for all threads to finish. If interrupted while waiting then it will continue to wait until the threads finish, before completing with the interrupt status set.
This method may only be invoked by the scope owner. If the scope is already closed then the scope owner invoking this method has no effect.
A
StructuredTaskScope
is intended to be used in a structured manner. If this method is called to close a scope before nested task scopes are closed then it closes the underlying construct of each nested scope (in the reverse order that they were created in), closes this scope, and then throwsStructureViolationException
PREVIEW. Similarly, if this method is called to close a scope while executing with scoped value bindings, and the scope was created before the scoped values were bound, thenStructureViolationException
is thrown after closing the scope. If a thread terminates without first closing scopes that it owns then termination will cause the underlying construct of each of its open tasks scopes to be closed. Closing is performed in the reverse order that the scopes were created in. Thread termination may therefore be delayed when the scope owner has to wait for threads forked in these scopes to finish.- Specified by:
close
in interfaceAutoCloseable
- Throws:
IllegalStateException
- thrown after closing the scope if the scope owner did not attempt to join after forkingWrongThreadException
- if the current thread is not the scope ownerStructureViolationExceptionPREVIEW
- if a structure violation was detected
-
StructuredTaskScope
when preview features are enabled.