CodeGym /Cours /JAVA 25 SELF /Structured Concurrency

Structured Concurrency

JAVA 25 SELF
Niveau 58, Leçon 0
Disponible

1. Introduction

Quand les threads jouent chacun leur propre partie

La programmation multithread classique ressemble souvent à une répétition sans chef d’orchestre. Chaque thread — comme un musicien — joue sa mélodie sans écouter les autres. L’un a terminé plus tôt et est parti fumer, un autre est resté bloqué sur un accord, un troisième s’est trompé de notes et a produit une erreur. Le résultat n’est pas une symphonie, mais une cacophonie : comprendre où chacun s’est trompé est presque impossible, et arrêter tout le monde d’un coup — c’est une vraie quête.

Structured Concurrency résout ce problème. Elle transforme des threads dispersés en un véritable ensemble : toutes les tâches sont réunies sous un seul « chef d’orchestre ». S’il donne le signal d’arrêt — l’orchestre se tait. Si un musicien se plante — les autres s’arrêtent proprement, sans briser l’harmonie générale. Tous les résultats et erreurs sont collectés de manière centralisée, au lieu d’être éparpillés dans les recoins du code.

Imaginez : vous ne laissez pas les musiciens jouer chacun de leur côté, mais vous les rassemblez dans une salle. Il y a un chef, une partition, et même si la trompette sonne faux — l’orchestre ne déraille pas et termine l’exécution proprement.

Ce que Structured Concurrency apporte

  • Un « scope » unique de tâche : toutes les sous-tâches vivent dans un même bloc de code, leur cycle de vie est borné par ce bloc.
  • Une terminaison prévisible : le thread parent ne se termine pas tant que toutes les sous-tâches ne sont pas terminées.
  • Annulation centralisée : si une tâche échoue ou si le parent décide de terminer — toutes les sous-tâches sont annulées correctement.
  • Gestion cohérente des erreurs : les erreurs des sous-tâches sont agrégées, on peut obtenir un « arbre des causes » (tree of causes).
  • Code propre et lisible : pas de threads « pendus », pas de tâches oubliées, pas de courses à l’annulation.

Structured Concurrency n’est pas seulement une nouvelle API, c’est une nouvelle façon de penser : les tâches doivent être structurées comme des blocs de code ordinaires (par exemple, try-with-resources).

2. Statut de Structured Concurrency dans Java

Au moment de la rédaction, Structured Concurrency est au statut Preview (Java 21–23), mais l’on s’attend à une évolution vers GA (General Availability) dans Java 24/25. L’API se trouve dans le package jdk.incubator.concurrent. Avant toute utilisation en production, vérifiez impérativement les notes de version de votre JDK !

Classes principales :

  • StructuredTaskScope — classe de base pour gérer un groupe de tâches.
  • Variantes : StructuredTaskScope.ShutdownOnFailure, StructuredTaskScope.ShutdownOnSuccess — politiques de terminaison des tâches.

Concepts clés de StructuredTaskScope

Modèle : fork, join et petite revue des résultats

Quand le chef (c’est-à-dire la tâche parente) donne le signal — les sous-tâches partent jouer leurs parties. Ce moment s’appelle le fork — comme si vous envoyiez les musiciens jouer leurs morceaux dans différentes salles.

Vient ensuite le join — le chef lève la baguette, et tous reviennent pour jouer l’accord final ensemble.

Après quoi, vous pouvez demander à chaque participant comment cela s’est passé :

  • via resultNow() obtenir immédiatement le résultat si tout s’est joué sans erreur ;
  • via throwIfFailed() — s’assurer que personne n’a joué faux. Si quelqu’un s’est malgré tout emmêlé dans les notes, une exception unique est levée — comme si le chef disait : « Nous avons une défaillance dans l’orchestre, on recommence ».

Politiques de terminaison

Tout chef a sa règle pour décider quand arrêter la musique. Dans Structured Concurrency, cela se définit par une politique de terminaison :

  • ShutdownOnFailure — si un musicien sort du rythme, le chef fait signe : « Stop ! On recommence ». Tous les autres cessent immédiatement de jouer.
  • ShutdownOnSuccess — à l’inverse, dès qu’un musicien a parfaitement joué sa partie, le chef est satisfait : « Ça suffit, inutile d’aller plus loin, nous avons déjà un gagnant ». Les autres se taisent — politique du premier résultat réussi.

Travail avec les threads virtuels

Chaque sous-tâche StructuredTaskScope est lancée dans un thread virtuel. C’est comme si vous aviez un orchestre où chaque musicien est docile et rapide, sans caprices ni exigences de scène. Vous pouvez en créer des centaines, des milliers — ce ne sont pas des threads lourds, mais des notes presque sans poids qui résonnent exactement quand il faut.

3. Exemple : agrégateur de requêtes HTTP

Considérons un cas pratique : nous avons trois sources de données (par exemple, trois serveurs différents), et nous voulons soit obtenir la réponse de toutes (et l’agréger), soit celle du premier qui répond avec succès.

Variante 1 : « Tous doivent réussir » (ShutdownOnFailure)

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorAllSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // on attend la fin de toutes les tâches
            scope.throwIfFailed(); // si au moins une a échoué — on lève une exception

            // Toutes les tâches ont réussi — on peut agréger les résultats
            String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
            System.out.println("Résultat agrégé : " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

Ce qui se passe :

  • Les trois tâches sont lancées en parallèle (dans des threads virtuels).
  • Si au moins l’une échoue — les autres sont annulées, une exception est levée.
  • Si toutes réussissent — on peut agréger les résultats en toute sécurité.

Variante 2 : « Succès au premier valide » (ShutdownOnSuccess)

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class AggregatorFirstSuccess {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> fetchFromSource1());
            Future<String> f2 = scope.fork(() -> fetchFromSource2());
            Future<String> f3 = scope.fork(() -> fetchFromSource3());

            scope.join(); // on attend le premier succès
            scope.throwIfFailed(); // si tous ont échoué — on lève une exception

            String result = scope.result(); // résultat de la première tâche réussie
            System.out.println("Premier résultat réussi : " + result);
        }
    }

    static String fetchFromSource1() { /* ... */ return "A"; }
    static String fetchFromSource2() { /* ... */ return "B"; }
    static String fetchFromSource3() { /* ... */ return "C"; }
}

Ce qui se passe :

  • Dès qu’une tâche se termine avec succès — les autres sont annulées.
  • Si toutes échouent — une exception est levée.

4. Annulation automatique et dégradation

StructuredTaskScope s’occupe lui-même d’annuler les tâches restantes si la politique l’exige. Par exemple, si une tâche a échoué (ShutdownOnFailure) ou si l’une s’est terminée avec succès (ShutdownOnSuccess), les autres reçoivent un signal d’annulation (interrupt).

Exemple : terminaison correcte avec délai d’expiration

import jdk.incubator.concurrent.StructuredTaskScope;
import java.time.Instant;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> fetchWithTimeout());
    Future<String> f2 = scope.fork(() -> fetchWithTimeout());

    scope.joinUntil(Instant.now().plusSeconds(2)); // on attend au plus 2 secondes
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println(result);
}

Si les tâches ne se sont pas terminées en 2 secondes — une exception sera levée et toutes les tâches seront annulées.

5. Erreurs et gestion des exceptions

Comment les exceptions des sous-tâches sont routées vers le scope

Parfois, pendant le concert, quelqu’un se trompe quand même de note — StructuredTaskScope ne fait pas semblant que rien ne s’est passé. Il enregistre soigneusement qui a joué à côté, puis transmet au chef un rapport complet. Quand vous appelez throwIfFailed(), il lance une exception agrégée — une sorte de rapport récapitulatif : « Voici la liste de ceux qui ont joué faux aujourd’hui ». Si nécessaire, vous pouvez dérouler cet « arbre des causes » et voir qui a exactement failli. Et si vous voulez savoir pour un exécuteur précis — Future.exceptionNow() dira comment sa partie s’est terminée.

Quand l’annulation n’est pas un échec

Important : l’annulation d’une tâche ne signifie pas toujours une erreur. Si le chef a dit « c’est fini, le concert est terminé », alors les musiciens rangent simplement leurs instruments — c’est cancelled, mais pas failed. Une erreur, c’est uniquement quand quelqu’un a réellement joué autre chose, et cette exception se retrouvera dans le rapport global.

Exemple : arbre des causes

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> { throw new RuntimeException("Erreur 1"); });
    Future<String> f2 = scope.fork(() -> { throw new RuntimeException("Erreur 2"); });

    scope.join();
    scope.throwIfFailed(); // lèvera une exception avec les deux causes
} catch (Exception e) {
    e.printStackTrace();
    // On peut obtenir les exceptions supprimées via e.getSuppressed()
}

6. Comparaison avec CompletableFuture

StructuredTaskScope et CompletableFuture — tous deux permettent de lancer des tâches parallèles, mais :

  • StructuredTaskScope est pratique quand les tâches sont logiquement liées et doivent se terminer/s’annuler ensemble (hiérarchie de tâches).
  • CompletableFuture est adapté pour la composition de tâches sans hiérarchie (par exemple, des chaînes de transformations, des scénarios réactifs).

Quand StructuredTaskScope simplifie le code :

  • Quand il faut garantir que toutes les sous-tâches sont terminées avant de sortir du bloc.
  • Quand une annulation centralisée et une gestion des erreurs sont nécessaires.
  • Quand il est important de ne laisser aucune tâche « pendue ».

Quand CompletableFuture est plus pratique :

  • Quand les tâches ne sont pas liées et peuvent vivre leur propre vie.
  • Quand une composition complexe est nécessaire (thenCombine, thenCompose, etc.).

7. Pratique : agrégateur de requêtes HTTP

Tâche : envoyer des requêtes à 3 sources, obtenir la première réponse réussie

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

public class HttpAggregator {
    public static void main(String[] args) throws Exception {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
            Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));
            Future<String> f3 = scope.fork(() -> httpRequest("https://api3.example.com"));

            scope.join();
            scope.throwIfFailed();

            String result = scope.result();
            System.out.println("Première réponse réussie : " + result);
        }
    }

    static String httpRequest(String url) throws Exception {
        // Simulation de la requête (on peut utiliser HttpClient)
        Thread.sleep((long) (Math.random() * 1000));
        if (Math.random() < 0.3) throw new RuntimeException("Erreur de requête : " + url);
        return "Réponse de " + url;
    }
}

Tâche : si une sous-tâche a échoué — éteindre correctement les autres

import jdk.incubator.concurrent.StructuredTaskScope;
import java.util.concurrent.Future;

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> f1 = scope.fork(() -> httpRequest("https://api1.example.com"));
    Future<String> f2 = scope.fork(() -> httpRequest("https://api2.example.com"));

    scope.join();
    scope.throwIfFailed();

    String result = f1.resultNow() + f2.resultNow();
    System.out.println("Les deux réponses : " + result);
} catch (Exception e) {
    System.err.println("Erreur dans l’une des tâches : " + e.getMessage());
}

8. Erreurs typiques avec StructuredTaskScope

Erreur n° 1 : vous avez oublié d’appeler join() ou throwIfFailed().
Si vous n’appelez pas join(), les tâches peuvent ne pas se terminer avant la sortie du bloc. Si vous n’appelez pas throwIfFailed(), les erreurs des sous-tâches passeront inaperçues.

Erreur n° 2 : tentative d’obtenir le résultat avant la fin de la tâche.
Appeler resultNow() avant la fin de la tâche lèvera IllegalStateException. Attendez d’abord la fin via join().

Erreur n° 3 : ignorer l’annulation.
Si une tâche a été annulée (par exemple, à cause de la politique du scope), n’essayez pas d’en obtenir le résultat — une exception sera levée.

Erreur n° 4 : mélanger différentes politiques de terminaison.
Il ne faut pas essayer d’annuler manuellement des tâches à l’intérieur d’un scope — utilisez les politiques ShutdownOnFailure ou ShutdownOnSuccess.

Erreur n° 5 : lancer de longues tâches gourmandes CPU dans des threads virtuels.
StructuredTaskScope utilise par défaut des threads virtuels — ils sont idéaux pour les tâches I/O-bound, mais n’accélèrent pas les calculs lourds.

Erreur n° 6 : oublier de fermer le scope (pas de try-with-resources).
StructuredTaskScope implémente AutoCloseable — utilisez toujours try-with-resources pour garantir la terminaison de toutes les tâches.

1
Mission
JAVA 25 SELF, niveau 58, leçon 0
Bloqué
Coordination des départements : projet "Module spatial" 🚀
Coordination des départements : projet "Module spatial" 🚀
1
Mission
JAVA 25 SELF, niveau 58, leçon 0
Bloqué
Recherche du serveur le plus rapide : système "Livraison de données supersonique" ⚡
Recherche du serveur le plus rapide : système "Livraison de données supersonique" ⚡
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION