CodeGym /Corsi /JAVA 25 SELF /Structured Concurrency

Structured Concurrency

JAVA 25 SELF
Livello 58, Lezione 0
Disponibile

1. Introduzione

Quando i thread suonano ognuno la propria parte

La concorrenza tradizionale spesso ricorda una prova senza direttore d’orchestra. Ogni thread è come un musicista che suona la propria melodia senza ascoltare gli altri. Qualcuno finisce prima e va a fumare, qualcuno resta bloccato su un accordo, qualcuno confonde le note e produce un errore. Il risultato non è una sinfonia, ma una cacofonia: capire dove ci si è persi è quasi impossibile, e fermare tutti insieme è un’impresa.

Structured Concurrency risolve questo problema. Trasforma thread sparsi in un vero ensemble: tutte le attività sono riunite sotto un unico «direttore». Se lui dà lo stop, l’orchestra tace. Se un musicista sbaglia, gli altri si fermano con ordine senza rovinare l’armonia complessiva. Tutti i risultati e gli errori vengono raccolti centralmente, invece di essere sparsi negli angoli del codice.

Immagina: non lasci i musicisti suonare come capita, ma li riunisci in una sala. C’è un direttore, c’è una partitura, e anche se la tromba stona, l’orchestra non va in crisi, ma conclude l’esecuzione in modo elegante.

Cosa offre Structured Concurrency

  • Un unico «scope» per il task: tutte le sottoattività vivono entro un unico blocco di codice, il cui ciclo di vita è delimitato da quel blocco.
  • Terminazione prevedibile: il thread padre non termina finché non terminano tutte le sottoattività.
  • Cancellazione centralizzata: se una attività fallisce o il padre decide di terminare, tutte le sottoattività vengono annullate correttamente.
  • Gestione coerente degli errori: gli errori delle sottoattività sono aggregati; è possibile ottenere un «albero delle cause» (tree of causes).
  • Codice pulito e leggibile: niente thread «pendenti», niente task dimenticati, niente corse alla cancellazione.

Structured Concurrency non è solo una nuova API, ma un nuovo modo di pensare: le attività devono essere strutturate come normali blocchi di codice (ad esempio, try-with-resources).

2. Stato di Structured Concurrency in Java

Al momento della stesura del corso Structured Concurrency è in stato Preview (Java 21–23), ma si prevede il passaggio a GA (General Availability) in Java 24/25. L’API si trova nel package jdk.incubator.concurrent. Prima di usarla in produzione, controlla sempre le note di rilascio aggiornate della tua versione di JDK!

Classi principali:

  • StructuredTaskScope — classe base per la gestione di un gruppo di task.
  • Varianti: StructuredTaskScope.ShutdownOnFailure, StructuredTaskScope.ShutdownOnSuccess — politiche di terminazione dei task.

Concetti fondamentali di StructuredTaskScope

Modello: fork, join e amichevole appello dei risultati

Quando il direttore (cioè l’attività padre) dà il segnale, le sottoattività partono sulle proprie parti. Questo momento si chiama fork: è come avviare i musicisti a suonare i loro pezzi in sale diverse.

Poi arriva il momento del join: il direttore alza la bacchetta e tutti tornano per suonare insieme l’accordo finale.

Dopodiché puoi chiedere a ciascun partecipante com’è andata:

  • con resultNow() ottieni il risultato subito, se tutto è andato a buon fine;
  • con throwIfFailed() ti assicuri che nessuno abbia stonato. Se qualcuno si è confuso tra le note, viene lanciata un’unica eccezione — come se il direttore dicesse: «Abbiamo un problema in orchestra, si ricomincia».

Politiche di terminazione

Ogni direttore ha le proprie regole su quando fermare la musica. In Structured Concurrency questo è definito da una politica di terminazione:

  • ShutdownOnFailure — se anche un solo musicista perde il ritmo, il direttore fa cenno: «Stop! Si ricomincia». Tutti gli altri smettono di suonare subito.
  • ShutdownOnSuccess — al contrario, non appena qualcuno esegue alla perfezione la sua parte, il direttore è soddisfatto: «Basta così, non serve altro, abbiamo già un vincitore». Gli altri tacciono — politica del primo esito positivo.

Uso dei thread virtuali

Ogni sottoattività StructuredTaskScope la esegue in un thread virtuale. È come avere un’orchestra in cui ogni musicista è rapido e collaborativo, senza capricci né esigenze di palco. Puoi creare tranquillamente centinaia o migliaia di questi esecutori — non sono thread pesanti, ma quasi note leggere che suonano esattamente quando serve.

3. Esempio: aggregatore di richieste HTTP

Consideriamo un caso pratico: abbiamo tre fonti di dati (ad esempio tre server diversi) e vogliamo ottenere la risposta o da tutte (per poi aggregarla) o dal primo che risponde con successo.

Variante 1: «Devono riuscire tutti» (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(); // aspettiamo il completamento di tutti i task
            scope.throwIfFailed(); // se anche uno è fallito: lanciamo un'eccezione

            // Tutti i task sono riusciti: possiamo aggregare i risultati
            String result = f1.resultNow() + f2.resultNow() + f3.resultNow();
            System.out.println("Risultato aggregato: " + result);
        }
    }

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

Cosa succede:

  • Tutte e tre le attività vengono avviate in parallelo (in thread virtuali).
  • Se almeno una fallisce, le altre vengono annullate e viene lanciata un’eccezione.
  • Se tutte hanno successo, è possibile aggregare i risultati in sicurezza.

Variante 2: «Successo al primo valido» (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(); // aspettiamo il primo esito positivo
            scope.throwIfFailed(); // se tutti sono falliti: lanciamo un'eccezione

            String result = scope.result(); // risultato della prima attività riuscita
            System.out.println("Primo risultato riuscito: " + result);
        }
    }

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

Cosa succede:

  • Non appena un’attività termina con successo, le altre vengono annullate.
  • Se tutte falliscono, viene lanciata un’eccezione.

4. Cancellazione automatica e degradazione

StructuredTaskScope si occupa automaticamente di annullare le attività rimanenti, se la politica lo richiede. Ad esempio, se un’attività è fallita (ShutdownOnFailure) o una è terminata con successo (ShutdownOnSuccess), le altre attività ricevono un segnale di cancellazione (interrupt).

Esempio: terminazione corretta con timeout

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)); // aspettiamo al massimo 2 secondi
    scope.throwIfFailed();

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

Se i task non si sono conclusi entro 2 secondi, verrà lanciata un’eccezione e tutte le attività saranno annullate.

5. Errori e gestione delle eccezioni

Come le eccezioni delle sottoattività vengono instradate nello scope

A volte durante il concerto qualcuno sbaglia una nota — StructuredTaskScope non fa finta di niente. Registra con precisione chi ha steccato e poi consegna al direttore un report completo. Quando chiami throwIfFailed(), lancia un’eccezione aggregata — una sorta di report riepilogativo: «Ecco l’elenco di chi oggi ha stonato». Se necessario, puoi espandere questo «albero delle cause» per vedere chi ha deluso. E se vuoi sapere del singolo esecutore, Future.exceptionNow() ti dirà come si è conclusa la sua parte.

Quando l’annullamento non è un fallimento

È importante ricordare: annullare un’attività non significa sempre errore. Se il direttore dice «il concerto è finito», i musicisti semplicemente ripongono gli strumenti — è cancelled, non failed. Si considera errore solo quando qualcuno ha suonato davvero la nota sbagliata, e quell’eccezione finirà nel riepilogo.

Esempio: albero delle cause

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

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

    scope.join();
    scope.throwIfFailed(); // lancerà un'eccezione con entrambe le cause
} catch (Exception e) {
    e.printStackTrace();
    // È possibile ottenere le eccezioni soppresse tramite e.getSuppressed()
}

6. Confronto con CompletableFuture

StructuredTaskScope e CompletableFuture — entrambi permettono di avviare attività in parallelo, ma:

  • StructuredTaskScope è comodo quando le attività sono logicamente correlate e devono terminare/essere annullate insieme (gerarchia di task).
  • CompletableFuture è utile per comporre attività senza gerarchia (ad esempio catene di trasformazioni, scenari reattivi).

Quando StructuredTaskScope semplifica il codice:

  • Quando occorre garantire che tutte le sottoattività siano terminate prima di uscire dal blocco.
  • Quando serve cancellazione centralizzata e gestione degli errori.
  • Quando è importante che non restino task «pendenti».

Quando CompletableFuture è più comodo:

  • Quando le attività non sono correlate e possono vivere di vita propria.
  • Quando serve una composizione complessa (thenCombine, thenCompose ecc.).

7. Pratica: aggregatore di richieste HTTP

Obiettivo: inviare richieste a 3 fonti e ottenere la prima risposta valida

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("Prima risposta valida: " + result);
        }
    }

    static String httpRequest(String url) throws Exception {
        // Simulazione della richiesta (si può usare HttpClient)
        Thread.sleep((long) (Math.random() * 1000));
        if (Math.random() < 0.3) throw new RuntimeException("Errore della richiesta: " + url);
        return "Risposta da " + url;
    }
}

Obiettivo: se una sottoattività fallisce, spegnere correttamente le altre

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("Entrambe le risposte: " + result);
} catch (Exception e) {
    System.err.println("Errore in uno dei task: " + e.getMessage());
}

8. Errori tipici nell’uso di StructuredTaskScope

Errore n. 1: hai dimenticato di chiamare join() o throwIfFailed().
Se non chiami join(), i task potrebbero non terminare prima dell’uscita dal blocco. Se non chiami throwIfFailed(), gli errori delle sottoattività passeranno inosservati.

Errore n. 2: tentativo di ottenere il risultato prima che il task sia concluso.
Chiamare resultNow() prima che il task sia terminato lancerà IllegalStateException. Prima attendi la fine tramite join().

Errore n. 3: ignorare l’annullamento.
Se un task è stato annullato (ad esempio a causa della politica dello scope), non tentare di ottenerne il risultato — verrà lanciata un’eccezione.

Errore n. 4: mescolare diverse politiche di terminazione.
Non annullare manualmente i task all’interno dello scope — usa le politiche ShutdownOnFailure o ShutdownOnSuccess.

Errore n. 5: avviare task CPU-bound lunghi nei thread virtuali.
StructuredTaskScope usa di default thread virtuali — sono ideali per task I/O-bound, ma non accelerano i calcoli pesanti.

Errore n. 6: hai dimenticato di chiudere lo scope (manca try-with-resources).
StructuredTaskScope implementa AutoCloseable — usa sempre try-with-resources per garantire la terminazione di tutti i task.

1
Compito
JAVA 25 SELF, livello 58, lezione 0
Bloccato
Coordinamento dei Reparti: Progetto "Modulo Spaziale" 🚀
Coordinamento dei Reparti: Progetto "Modulo Spaziale" 🚀
1
Compito
JAVA 25 SELF, livello 58, lezione 0
Bloccato
Ricerca del server più veloce: Sistema "Consegna dati supersonica" ⚡
Ricerca del server più veloce: Sistema "Consegna dati supersonica" ⚡
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION