The core concept of so-called 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 in virtual threads then those virtual 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 virtual 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 that avoid the need for 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.newUnbounedExector(factory)) {executor.schedule(task1);executor.schedule(task2);}
The thread executing this code will block in the executor’s close method until the executor has terminated.
This structured lead itself to nesting or to 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.
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.