30 augustus 2023
|Structured Concurrency in Java is een van de features die in Java 21 wordt uitgerold. Het idee is niet nieuw maar wordt nu voor het eerst in preview uitgerold. In deze blogpost zullen we de kracht van structured concurrency laten zien.
Structured concurrency introduceerd een nieuw manier van concurrent programmeren en de primaire features zijn:
Om structured concurrency beter te begrijpen is het belangrijk om eerst te kijken hoe concurrency werkt zonder structured concurrency. Stel je voor dat je een stuk code hebt dat verschillende taken moet uitvoeren en elke moet zijn eigen acties bevatten:
Thread task1 = new Thread(() -> {
// Code for Task 1
});
Thread task2 = new Thread(() -> {
// Code for Task 2
});
task1.start();
task2.start();
In dit voorbeeld initialiseren en managen we elke taak individueel. Hierdoor creëeren we code duplication, verhogen we het risico op uncaught errors, en verhogen we het risico op resource leaks. Met structured concurrency kunnen we dit op een gestructureerde manier afhandelen:
try (StructuredTaskScope scope = new StructuredTaskScope()) {
scope.fork(() -> {
// Code for Task 1
});
scope.fork(() -> {
// Code for Task 2
});
scope.join();
}
In dit geval gebruiken we 'StructuredTaskScope' om verschillende taken te managen. Hierdoor hebben we minder code en wordt de code leesbaarder. Omdat alles binnen één scope wordt behandeld hoeven we ons maar een keer zorgen te maken over initialisatie, monitoring, cancellation en errors.
Een van de meest krachtige features van structured concurrency is de manier om errors af te handelen. In traditionele concurrency kon het lastig zijn om alle errors af te handelen binnen verschillende threads. Errors in onafhankelijke threads moeten individueel worden afgehandeld of gebruiken complexe mechanismes om errors naar de main thread te propogeren.
Een voorbeeld van errors in de tradionele concurrency:
Thread task1 = new Thread(() -> {
try {
// Code for Task 1
} catch (Exception e) {
// Handle exception for Task 1
}
});
Thread task2 = new Thread(() -> {
try {
// Code for Task 2
} catch (Exception e) {
// Handle exception for Task 2
}
});
task1.start();
task2.start();
In het bovenstaande voorbeeld heeft iedere thread zijn eigen error handling. Naast duplication, kan het ook zijn dat exceptions uit één van de threads niet direct worden afgehandeld in de main thread. Hierdoor kunnen processsen door blijven lopen die eigenlijk gestopt moeten worden.
Met structured concurrency, kunnen we de errors veel intuïtiever afhandelen:
try (StructuredTaskScope scope = new StructuredTaskScope()) {
scope.fork(() -> {
// Code for Task 1
});
scope.fork(() -> {
// Code for Task 2
});
scope.join();
} catch (StructuredTaskException e) {
// Handle exceptions from any of the tasks
for (Throwable t : e.getSuppressed()) {
// Process individual exceptions
}
}
Als een van de taken binnen de 'StructuredTaskScope' een error heeft, dan wordt deze netjes binnen de StructuredTaskException afgehandeld. Deze exception kan verschillende exceptions afhandelen, en deze op eenduidige manier propogeren naar de main thread. Hierdoor wordt error handling niet allen gecentraliseerd, het schets ook een helder plaatje rondom errors, hierdoor wordt debugging en error management eenvoudiger.
HetMet managen ean taken word is het nodig om complexe control flows te gebruiken om de relatie tussen taken te managen. Callbacks zijn hier een goed voorbeeld van. Met structured concurrency wordt dit vereenvoudigd omdat er een duidelijke relatie is tussen taken binnen dezelfde scope.
Future<?> future = executorService.submit(() -> {
// Task code
});
// Later in the code
future.cancel(true);
Met structured concurrency wordt het annuleren van taken veel eenvoudiger. Kijk maar naar het onderstaande voorbeeld:
try (StructuredTaskScope scope = new StructuredTaskScope()) {
scope.fork(() -> {
// Task code
});
scope.shutdown();
}
In dit voorbeeld worden de taken binnen de 'StructuredTaskScope' eenvoudiger geannuleerd met minder regels code.
Structured concurrency verandert niet alleen de manier waarop taken worden beheerd, het maakt de staat en het gedrag van gelijktijdige taken transparant. Observatie, in de context van structured concurrency, gaat in op taken uitvoeren, debuggen en inzichten verkrijgen in de uitvoering van taken. Bij traditionele concurrency vereist het observeren van individuele threads of taken vaak externe tools, aangepaste logmechanismen of ingewikkelde debuggingtechnieken. Een voorbeeld van logging binnen traditionele concurrency
Thread task1 = new Thread(() -> {
// Code for Task 1
System.out.println("Task 1 completed");
});
Thread task2 = new Thread(() -> {
// Code for Task 2
System.out.println("Task 2 completed");
});
task1.start();
task2.start();
Door gebruik te maken van simpele print statements krijg je als developer inzicht in de voortgang van de taak. Er mist alleen een hollistisch inzicht van de gehele groep van taken en hun afhankelijkheden. Hierdoor is het lastiger om fouten in deze processen te vinden of iets te kunnen zeggen over de staat van het proces. Met structured concurrency wordt observability eenvoudig gemaakt:
try (StructuredTaskScope scope = new StructuredTaskScope()) {
scope.fork(() -> {
// Code for Task 1
});
scope.fork(() -> {
// Code for Task 2
});
scope.join();
System.out.println("All tasks within the scope completed");
}
Met structured concurrency, wordt het mogelijk om de taken op scope niveau te monitoren. Verder implementaties van structured concurrency zullen APIs bevatten om real-time metrics, states of traces binnen de scope te kunnen monitoren.
Structured concurrency is een krachtige java feature die het mogelijk maakt om concurrency in op een expressieve en leesbare manier te programeren. De verschillende voordelen kunnen worden samengevat als: