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 scheduled to be executed in fibers then those fibers must terminate before the main task can complete.
The main benefit of structured concurrency is abstraction. A caller of a method that is invoked to do a task should not care if the method decomposes the task and schedules a million fibers. When the method completes, any fibers scheduled by the method should have terminated.
Current prototype
In Project Loom, a prototype API has been developed called FiberScope that is a scope in which fibers are scheduled. Here is a basic example:
try (var scope = FiberScope.open()) { var fiber1 = scope.schedule(task); var fiber2 = scope.schedule(task); }
A thread or fiber creates and enters a scope by calling the FiberScope.open method. It exits the scope when the code in the block completes and any fibers scheduled in the scope have terminated. The example schedules two fibers. The thread/fiber executing the above code may have to wait (in the FiberScope’s close method) until the two fibers have terminated.
FiberScopes can be nested, consider the following:
try (var scope1 = FiberScope.open()) { scope1.schedule(task); try (var scope2 = FiberScope.open()) { scope2.schedule(task); } scope1.schedule(task); }
In this example, a thread or fiber enters scope1, schedules a fiber, then enters scope2 where it schedules a fiber in that scope. The execution cannot exit scope2 until the fiber scheduled in that scope terminates. When it exits, it is back in scope1, where it schedules another fiber. It cannot exit scope1 until the two fibers scheduled in that scope have terminated.
More generally, the structured approach leads to trees of tasks. Consider the following method x that schedules two fibers to execute foo and bar. The code in foo and bar each schedule two fibers.
void x() { try (var scope1 = FiberScope.open()) { var fiber1 = scope1.schedule(() -> foo()); var fiber2 = scope1.schedule(() -> bar()); } } void foo() { try (var scope2 = FiberScope.open()) { scope2.schedule(() -> task()); scope2.schedule(() -> task()); } } void bar() { try (var scope3 = FiberScope.open()) { scope3.schedule(() -> task()); scope3.schedule(() -> task()); } }
fiber1 is scheduled in scope1 to execute foo. It enters scope2 and schedules two fibers. It exits scope2 (and returns to scope1) when the two fibers terminate. fiber2 is scheduled in scope1 to execute bar. It enters scope3 and schedules two fibers. It exits scope3 (and returns to scope1) when the two fibers terminate. The thread or fiber executing will not exit scope1 until fiber1 and fiber2 have terminated.
FiberScope API also defines the static background() method to return the background scope which can be used to schedule fibers that are intended to outline the context where they are initially scheduled.
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.
Deadline/Timeouts
Decomposing deadline or timeouts is very difficult to get right.
FiberScope supports creating and entering a scope with a deadline. If the deadline is reached before the thread/fiber exits the scope then all fibers scheduled in the scope are cancelled.
Deadlines work with nested scopes. Consider the following:
var deadline = Instant.now().plusSeconds(10); try (var scope1 = FiberScope.open(deadline)) { try (var scope2 = FiberScope.open()) { scope2.schedule(() -> task()); } }
scope1 is entered with a deadline that is now + 10s. It schedules a fiber in scope2 and cannot exit to scope1 until the fiber terminates. If the deadline is reached in the meantime then the fiber will be cancelled.
FiberScope also defines open(Duration timeout) to enter a scope with a timeout. If the timeout expires before thread/fiber exits the scope then all fibers scheduled in the scope are cancelled.
FiberScope Variables/Locals
There is no support at this time for making context available to all fibers scheduled in the scope. InheritedThreadLocals can be used in the mean-time.
Further reading
Nathaniel J. Smith: Notes on structured concurrency, or: Go statement considered harmful
Nathaniel J. Smith: Timeouts and cancellation for humans
Martin Sustrik: Structured Concurrency