Introducing scope object (StructuredTaskScope) – Concurrency – Virtual Threads, Structured Concurrency
220. Introducing scope object (StructuredTaskScope)
So far, we have covered a bunch of problems that use virtual threads directly or indirectly via an ExecutorService. We already know that virtual threads are cheap to create and block and that an application can run millions of them. We don’t need to reuse them, pool them or do any fancy stuff. Use and throw is the proper and recommended way to deal with virtual threads. This means that virtual threads are very useful for expressing and writing asynchronous code which is commonly based on a lot of threads that are capable of blocking/unblocking many times in a short period of time. On the other hand, we know that OS threads are expensive to create, very expensive to block, and are not easy to put in an asynchronous context.Before virtual threads (so, for many many years), we had to manage the lifecycle of OS threads via an ExecutorService/Executor and we could write asynchronous (or reactive) code via callbacks (you can find detailed coverage of asynchronous programming in Java Coding Problems, First Edition, Chapter 11).However, asynchronous/reactive code is hard to write/read, very hard to debug and profile, and almost deadly hard to unit test. Nobody wants to read and fix your asynchronous code! Moreover, once we start to write an application via asynchronous callback we tend to use this model for all tasks, even for those that shouldn’t be asynchronous. We can easily fall into this trap when we need to link somehow asynchronous code/results to non-asynchronous code. And, the easiest way to do it is to go only for asynchronous code.So, is there a better way? Yes, it is! Structured Concurrency should be the right answer. Structured Concurrency has started as an incubator project and reached the preview stage in JDK 21 (JEP 453).And, in this context, we should introduce StructuredTaskScope. A StructuredTaskScope is a virtual thread launcher for Callable tasks that returns a Subtask. A subtask is an extension of the well-known Supplier<T> fuctional interface represented by the StructuredTaskScope.Subtask<T> interface and forked with StructuredTaskScope.fork(Callable task). It follows and works based on the fundamental principle of structured concurrency (see Problem 203) – When a task has to be solved concurrently then all the threads needed to solve it are spun and rejoined in the same block of code. In other words, all these threads’ lifetime is bound to the block’s scope, so we have clear and explicit entry-exit points for each concurrent code block. These threads are responsible to run subtasks (Subtask) of the given task as a single unit of work.Let’s have an example of fetching a single tester (with id 1) from our web server via StructuredTaskScope:
public static TestingTeam buildTestingTeam()
throws InterruptedException {
try (StructuredTaskScope scope
= new StructuredTaskScope<String>()) {
Subtask<String> subtask
= scope.fork(() -> fetchTester(1));
logger.info(() -> “Waiting for ” + subtask.toString()
+ ” to finish …\n”);
scope.join();
String result = subtask.get();
logger.info(result);
return new TestingTeam(result);
}
}