Java: Structured Concurrency in Java 21


calendar icon

30 augustus 2023

|
calendar icon 7 minuten

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.

Waarom structured concurrency?

Structured concurrency introduceerd een nieuw manier van concurrent programmeren en de primaire features zijn:

  1. Task scoping: Met structured concurrency creëer je een scope voor al je taken. Alle taken binnen deze scope worden als een unit behandeld, er is een parent child relatie tussen scope en onderliggende taken.
  2. Gestroomlijnde error handling: Errors binnen de scope kunnen op een uniforme manier worden afgehandeld en verwerkt. Als een taak faalt, dan kan de impact van deze error eenvoudig afhandelen binnen de scope.
  3. Verbeterde Observability: De gestructureerde aanpak biedt beter inzicht in de uitvoering van taken, en maakt monitoring en debugging makkelijker.
  4. Vereenvoudigde Cancellation: Met traditionele concurrency 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.

Structured Task Scope

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.

Error handling binnen structured concurrency

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.

Vereenvoudigde Cancellation

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.

Verbeterde Observability

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.

Conclusie

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:

  • Veiligheid: Bij traditionele concurrency kunnen taken blijven openstaan. Dit kan leiden tot resource leaks of ongewenst gedrag.Structured concurrency zorgt ervoor dat alle taken vanuit de parent scope worden afgehandeld, wordt de parent gecancelled dan worden de children ook gecancelled.
  • Leesbaarheid: Door taken binnen een duidelijke hierarchie te organiseren wordt de code leesbaar. Het is eenvoudiger om de relatie tussen taken te begrijpen waardoor de onderhoudbaarheid van de code wordt vergroot.
  • Error Propagatie: In een structured omgeving worden fouten binnen een child taak gepropogeerd naar de parent taak. Deze uniforme manier van error handling vereenvoudigd debugging en dwingt af dat fouten op het juiste niveau worden afgehandeld.
  • Resource Management: Met structured concurrency worden resource op een voorspelbare wijze georchestreerd. Hierdoor worden resources efficienter gebruikt en wordt het risico op resource leaks verkleind.
  • Vereenvoudigde Control Flow: Met Traditionel concurrency 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.
  • Consistente State: Omdat child taken verbonden zijn aan de lifecycle van de parent taak wordt het risico op inconsistente states verminderd. Hierdoor worden processen veiliger en voorspelbaarder.

    Structured concurrency is een game-changer voor de manier waarop developers concurrency kunnen aanpakken in Java. Het proces is gestroomlijnder, efficienter, veiliger en developer-friendly.
Deel:

Recente blogs