The core concept of "structured concurrency" is that when control splits into concurrent tasks that they join up again. If a main task splits into several concurrent sub-tasks to be executed by spawning threads then those threads must terminate before the main task can complete. The benefit is abstraction. The caller of a method that is invoked to do a task should not care if the method decomposes the work into sub-tasks that are executed by a million threads. When the method completes then all threads scheduled by the method should have terminated.
An early of prototype of Project Loom had API named FiberScope to support the scheduling of fibers (a precursor to virtual threads) with initial support in this area. There is no explicit support in the current prototype but it is possible to use existing constructs without needing too many new APIs. In particular, ExecutorService has been retrofitted to extend AutoCloseable so that it’s possible to write code like this:
Thread factory = Thread.builder().virtual().factory()try (ExecutorService executor = Executors.newUnboundedExecutor(factory)) {executor.schedule(task1);executor.schedule(task2);}
A thread executing this code will block in the executor’s close method until the two tasks have have completed and executor has terminated.
This structured lends itself to nesting or even a tree of tasks:
try (ExecutorService executor = Executors.newUnboundedExecutor(factory)) {
executor.submit(() -> foo());
executor.submit(() -> bar());
}
void foo() {
try (ExecutorService executor = Executors.newUnboundedExecutor(factory)) {
executor.submit(...);
}}void bar() {
try (ExecutorService executor = Executors.newUnboundedExecutor(factory)) {
executor.submit(...);}
}
In this example, the main task will not complete until foo and bar complete. The foo method, executing in a virtual thread, does not complete until the task that is submitted completes. Similarly bar, executing in another virtual thread, does not complete until the task that it submitted completes.
Cancellation
A FiberScope can be created with options that configure how cancellation is handled. At this time, the options are PROPAGATE_CANCEL, CANCEL_AT_CLOSE, and IGNORE_CANCEL.
The following example uses the PROPAGATE_CANCEL option:
try (var scope = FiberScope.open(Option.PROPAGTE_CANCEL)) { var fiber1 = scope.schedule(task); var fiber2 = scope.schedule(task); }
If a fiber executing in this scope is cancelled then it will also cancel fiber1 and fiber2.
The IGNORE_CANCEL option is for recovery/cleanup tasks that cannot be cancelled. The Fiber.cancelled() method always returns false when executing in scope created with this option.
Deadlines
An ExecutorService can be created with a deadline so that its shutdowNow method is invoked if the deadline expires before the executor has terminated.
Thread factory = Thread.builder().virtual().factory()var deadline = Instant.now().plusSeconds(10);try (ExecutorService executor = Executors.newUnboundedExector(factory).withDeadline(deadline)) {executor.schedule(task1);executor.schedule(task2);}
Deadlines do not correctly work with nested usages.