Parte II — Programmazione asincrona e reattiva · Capitolo 10

Programmazione asincrona

~45 min di lettura12 widget interattivi

In questo capitolo

  1. Motivazioni e panorama dei modelli asincroni
  2. Event-driven programming e l'event loop
  3. Il Reactor pattern
  4. La regola del never-blocking
  5. Callback e Continuation-Passing Style
  6. Callback hell e Pyramid of Doom
  7. Promise: stati e API
  8. Promise chaining e composizione
  9. Limiti delle Promise
  10. Async/await
  11. Coroutine e fiber
  12. Platform thread e virtual thread in Java
  13. Verifica le tue conoscenze

1. Motivazioni e panorama dei modelli asincroni

La programmazione asincrona è uno stile di programmazione sempre più importante e pervasivo, supportato ormai da tutti i principali framework e piattaforme (Java, .NET, iOS, Android, JavaScript/Node.js). Il suo cuore è la gestione di computazioni, richieste o processi che vengono eseguiti in modo non bloccante, astraendo il più possibile dai thread fisici del sistema operativo.

La programmazione asincrona non si sostituisce ai thread, ma offre un modo diverso di pensare alla concorrenza, spesso più adatto a scenari I/O-bound e reattivi. L'evoluzione storica mostra una ricerca costante di pattern migliori: Microsoft è passata dall'Asynchronous Programming Model (APM) all'Event-based Asynchronous Pattern (EAP) con .NET 2.0, fino al Task-based Asynchronous Pattern (TAP) con .NET 4.5.

Per l'esame

Il primo assignment richiede di proporre una soluzione concorrente basata su thread e, come versione successiva, di rivederla usando task ed executor dove ritenuto opportuno. La comprensione della separazione tra logica (cosa va eseguito in parallelo) e fisica (quanti thread/core sono disponibili) è un punto centrale della valutazione.

Il panorama della programmazione asincrona include:

  • Event-driven programming — architetture basate su event loop
  • Reactor pattern — dispatcher per eventi asincroni
  • Async Functions & CPS — continuation-passing style con callback
  • Promise — oggetti proxy per risultati futuri
  • Async/await — linguaggio che mimano la sincronia
  • Coroutine — thread leggeri a scheduling cooperativo

Oggi la programmazione asincrona è supportata nativamente da tutti i linguaggi moderni: JavaScript (Promise, async/await), Python (asyncio, coroutine), C# (Task, async/await), Java (CompletableFuture, virtual thread), Kotlin (coroutine), Dart, Swift, Rust, e molti altri.

Il dibattito storico "events vs threads" ha attraversato decenni, con posizioni forti da entrambi i lati (Ousterhout, Lee, von Behren). La vera sfida è capire come metterli insieme.

In forma tabellare, le quattro grandi famiglie di approcci:

ApproccioDescrizione
Event-drivenArchitetture basate su event-loop, pattern reactor
Async Functions & CPSEvent handler come continuazioni, supporto librerie Promise
Async/awaitMeccanismo linguistico che imita la sincronia pur essendo asincrono
Coroutine-basedThread leggeri, cooperativi

Eventi vs Thread: un dibattito storico

Il professor Ricci cita il dibattito fondamentale tra eventi e thread:

Questi lavori hanno mostrato come l'approccio event-driven offra vantaggi in termini di assenza di race condition e deadlock a basso livello, mentre i thread rimangono insostituibili per lo sfruttamento del parallelismo hardware. Ogni approccio ha pro e contro: la vera sfida è come metterli insieme.

Un secondo sguardo alle quattro famiglie, con il loro ruolo architetturale:

Architettura a event loop: un singolo flusso di controllo attende eventi e dispaccia handler. È la base del Reactor Pattern. Esempi: JavaScript nel browser, Node.js, Vert.x, tutte le GUI.

Funzioni asincrone con continuation-passing style: la callback è l'ultimo parametro e viene invocata quando il risultato è pronto. Soffre del problema della "pyramid of doom" quando si annidano chiamate asincrone.

Oggetti proxy che rappresentano un valore futuro. Permettono di appiattire l'annidamento delle callback grazie al chaining (.then()). Una promise può essere risolta o rifiutata una sola volta (immutabile).

Estensione linguistica che permette di scrivere codice asincrono con sintassi sincrona usando le parole chiave async e await. Sotto il cofano usa le coroutine. Supportata ormai da tutti i linguaggi moderni (JavaScript, Python, C#, Kotlin, Dart, Rust).

Idea chiave

Il professore sottolinea che questi modelli non sono in competizione: gli event loop sono l'architettura di esecuzione, le promise sono il meccanismo di composizione, e async/await è la sintassi che rende il tutto leggibile. Ciascuno opera a un diverso livello di astrazione.

2. Event-driven programming e l'event loop

Un programma event-driven è fatto di gestori di eventi (event handler) che vengono chiamati (eseguiti, attivati) quando si verificano eventi. Gli eventi possono riferirsi a qualsiasi cambiamento di stato o input dall'ambiente rilevante per il programma: azioni utente (click, pressioni di tasti), output di sensori, messaggi, risposte ricevute. Una caratteristica fondamentale è che il flusso di controllo dipende dagli eventi, introducendo non-determinismo. Si parla anche di "programming without a call stack": dopo l'esecuzione di ogni handler lo stack è vuoto.

Modello di esecuzione: l'event loop

L'event loop è un'architettura di controllo in cui un singolo thread di controllo attende eventi ed esegue gli handler corrispondenti. Una coda degli eventi tiene traccia degli eventi generati dall'ambiente o dagli handler stessi. Gli handler sono eseguiti atomicamente: se un evento si verifica mentre un handler è in esecuzione, sarà servito solo al termine dell'handler corrente (nessuna concorrenza).

loop {
  Event ev = waitForEvent(eventQueue)
  Handler handler = selectHandler(ev)
  execute(handler)
}

Schema dell'Event Loop

Sorgenti eventi
Coda eventi
Event Loop
Handler
[Eventi: click, timer, I/O, message]

L'event loop estrae un evento dalla coda, seleziona l'handler registrato e lo esegue. Gli handler non devono mai bloccare.

flowchart LR
    EQ[("Coda eventi")] --> EL["Event Loop"]
    EL -->|"preleva evento"| H["Handler"]
    H -->|"esegue
atomicamente"| COMP COMP -->|"può generare
nuovi eventi"| EQ

Schema semplificato del modello event loop: un singolo thread attende eventi, esegue gli handler in modo atomico, e gli handler stessi possono generare nuovi eventi.

Esplorate gli stati dell'Event Loop nel modello astratto:

Il professore sottolinea un punto cruciale: in un modello event loop puro, non ci possono essere race condition. Perché? Perché non c'è condivisione di stato tra thread concorrenti: c'è un solo thread che accede allo stato, in modo sequenziale. Le race condition nascono solo se si introduce un meccanismo come async/await che permette a un blocco di codice di sospendersi e riprendersi, come vedremo più avanti.

L'event loop è il pattern che sta sotto JavaScript, Node.js e — guarda caso — l'event loop incapsulato di ogni attore: un singolo thread attende eventi, li dispatca agli handler appropriati e ogni handler viene eseguito fino al completamento prima di servire l'evento successivo.

Nota del redattore

L'event loop è il pattern unificante tra programmazione ad attori, programmazione asincrona JavaScript, e architetture reattive. In tutti i casi, la regola è: non bloccare mai l'event loop.

Per l'esame

La never-blocking rule è il fondamento della programmazione asincrona. Dovete saper spiegare perché un handler non si blocca mai, come vengono gestite le operazioni lunghe (delegate a thread esterni che poi ripubblicano eventi), e perché in un event loop puro non ci sono race.

3. Il Reactor pattern

Il Reactor pattern (Schmidt et al., Pattern-Oriented Software Architecture Volume 2, 2000) è il pattern architetturale che formalizza l'event loop come pattern di progettazione per sistemi concorrenti e distribuiti. Separa la demultiplazione e la dispatch degli eventi dalla logica applicativa. I partecipanti principali sono:

Il flusso di collaborazione: quando un Handle diventa "pronto", l'Initiation Dispatcher lo rileva, usa l'Handle come chiave per localizzare l'Event Handler appropriato, e chiama il metodo hook corrispondente.

sequenceDiagram
    participant Client as Client
    participant ID as Initiation Dispatcher
    participant EH as Event Handler
    participant CEH as Concrete EH

    Client->>ID: register handle + handler
    Client->>ID: event occurs on handle
    ID->>EH: handle_event(handle)
    EH->>CEH: perform app logic
    CEH-->>EH: result
    EH-->>ID: done
    
Nota del redattore

Il Reactor Pattern è la base su cui sono costruiti framework come Node.js, Vert.x, Netty e gran parte dei web server asincroni moderni. La sua potenza sta nel separare la gestione degli eventi (dispatcher, infrastruttura) dalla logica applicativa (handler): questa separazione è la chiave della sua flessibilità.

4. La regola del never-blocking

Regola fondamentale: Never-blocking

Gli event handler non devono mai bloccarsi (non devono contenere chiamate bloccanti) e devono sempre terminare (non devono contenere cicli infiniti). Una chiamata bloccante o un ciclo infinito bloccherebbero l'intero event loop, impedendo il processamento di tutti gli eventi successivi nella coda. Questa regola garantisce la reattività del sistema.

Cosa fare quando serve un'operazione che sarebbe bloccante (come leggere un file o fare una richiesta di rete)? La risposta è sostituirla con una richiesta o computazione asincrona, che genererà un evento in futuro (risultato o errore). Queste richieste asincrone sono servite da altri thread (indipendenti dall'event loop), che interagiscono con l'event loop inserendo eventi nella coda.

flowchart LR
      subgraph "Event Loop Thread"
        el[Event Loop] --> h[Handler]
        h --> |delega task bloccante| Pool
        h --> |torna in attesa| el
      end
      subgraph "Background Thread Pool"
        Pool[Worker Pool]
        Pool --> |task completato| coda[(Event Queue)]
      end
      coda --> |evento pronto| el
Idea chiave

Il professore spiega: "Non appena io devo fare un'operazione che so che è bloccante, non mi blocco: do il compito, delego il task a un worker pool. Ogni procedura bloccante viene trasformata in una procedura non bloccante async."

Multiple event loop

Un sistema può essere composto da più componenti attivi, ciascuno con il proprio event loop privato, la propria coda eventi e i propri task asincroni non bloccanti. In questo caso i componenti:

Idea chiave

Questa architettura a event loop multipli è esattamente quella che ritroveremo nel modello Actor e in framework come Vert.x, dove ogni verticle ha il proprio event loop e i verticle comunicano tramite messaggi asincroni sull'event bus.

5. Callback e Continuation-Passing Style

Dato il vincolo del non-blocco, come eseguire handler di lunga durata? La risposta è: eseguendo task/funzioni asincrone. L'handler attiva l'esecuzione di un task asincrono e prosegue senza attendere il risultato. Ma sorge il problema centrale: come elaborare i risultati o gli errori generati asincronamente? Attraverso il modello callback.

Una callback è una funzione specificata come argomento (tipicamente l'ultimo) di una chiamata a funzione/task asincrono. La callback viene invocata dall'event loop quando viene processato l'evento relativo al risultato o errore generato dall'esecuzione asincrona. In questo modo la callback definisce una continuazione della computazione, da cui il nome Continuation-Passing Style (CPS).

Il CPS non è nato con la concorrenza: è uno stile di programmazione che può essere applicato anche al codice sequenziale. L'idea è che ogni funzione, invece di restituire un risultato tramite return, prende un argomento extra — la continuazione — e le passa il risultato. In questo modo non serve più uno stack: la computazione diventa una sequenza di chiamate, senza mai tornare indietro.

CPS nei linguaggi funzionali

Il Continuation-Passing Style è uno stile di programmazione in cui il controllo viene passato esplicitamente sotto forma di continuazione. Introdotto da Gerald Jay Sussman e Guy L. Steele Jr. (1975) per Scheme. È un formato intermedio popolare per i compilatori di linguaggi funzionali, permette l'ottimizzazione delle chiamate tail e consente una programmazione "senza stack".

Ecco come si trasforma una funzione normale in CPS:

Notate che in CPS sum non ha più un tipo di ritorno: restituisce void perché il "risultato" viene passato alla continuazione. Il corpo calcola x + y e poi chiama cont(x + y). Nel caso asincrono, la continuazione viene chiamata non immediatamente, ma quando il risultato dell'operazione asincrona è disponibile.

// CPS in Java (esempio astratto dalle slide)
void sum(int x, int y, Consumer<Integer> cont) {
    cont.accept(x + y);
}
sum(1, 3, (res) -> { System.out.println(res); });
Stack-less programming

Nel CPS non si usa più lo stack per le chiamate di ritorno: ogni funzione chiama la continuazione e "non torna mai indietro". Questo rende il CPS un formato intermedio popolare per i compilatori di linguaggi funzionali, permettendo ottimizzazioni come la tail call optimization.

Il professore cita un aneddoto: "Spesso nei colloqui chiedono cosa vuol dire CPS e molti rispondono 'Continuous Passing Style' invece di 'Continuation Passing Style' — ma il senso è lo stesso: passare una funzione che rappresenta la continuazione della computazione."

L'esempio loadUserPic: da sincrono ad asincrono

function loadUserPic(userId) {
  let user = findUserById(userId);
  return loadPic(user.picId);
}

Versione sincrona e bloccante: ogni chiamata attende il risultato prima di procedere.

function loadUserPic(userId, ret) {
  findUserById(userId, (user) => {
    loadPic(user.picId, ret);
  });
}
loadUserPic('john', (pic) => {
  ui.show(pic);
});

Versione asincrona in CPS: findUserById e loadPic sono funzioni asincrone, l'ultimo argomento è la callback che riceve il risultato. Non c'è attesa bloccante.

La stessa trasformazione, riga per riga:

Callback come closure

Le callback sono spesso implementate come closure, una tecnica per il name binding lessicale in linguaggi con funzioni di prima classe.

Idea chiave

Una closure è un record che memorizza una funzione insieme all'ambiente lessicale (il mapping delle variabili libere) presente al momento della creazione. Questo permette alla callback di accedere alle variabili del contesto originale quando viene invocata dall'event loop, anche in un momento successivo.

Un esempio minimale: asyncInc

Il professore fa un esempio concreto in JavaScript, mostrando come una funzione asincrona come asyncInc prenda un valore e una callback, esegua l'incremento nel ciclo successivo dell'event loop (tramite setTimeout(..., 0)) e chiami la callback con il risultato.

Chi invoca la continuazione?

Un aspetto importante è chi invoca la continuazione/callback. Due possibilità:

  1. Un thread di controllo diverso da quello che ha attivato la richiesta (es. il thread che genera l'evento) — ma questo introduce potenziali race condition con lo stato condiviso.
  2. Nell'architettura event-loop, la continuazione viene eseguita dall'event-loop stesso, ossia dal thread logico che ha attivato il task asincrono (che è diverso dal thread che ha servito il task). Questo è il modello di esecuzione delle moderne app web (client e server) ed evita le race condition perché tutto passa attraverso un unico thread.

Il thread fisico che ha eseguito materialmente il task asincrono (es. una lettura di rete) è diverso, ma la continuazione torna sempre all'event loop — questo è il punto cruciale che evita le race condition.

Vantaggi del modello CPS con event-loop

6. Callback hell e Pyramid of Doom

Con le callback possiamo scrivere codice asincrono, ma la complessità emerge quando dobbiamo coordinare più operazioni. Due pattern fondamentali:

Sequenza (task1; task2)

asyncTask1(..., (res1) => {
  asyncTask2(..., (res2) => {
    ...
  })
})

Parallelo (task1 | task2)

asyncTask1(..., (res1) => { ... })
asyncTask2(..., (res2) => { ... })

Il problema emerge quando si annidano più callback in sequenza: la struttura del codice si espande orizzontalmente e verso destra invece che verticalmente. È la cosiddetta callback hell o pyramid of doom.

step1(function(result1) {
  step2(function(result2) {
    step3(function(result3) {
      // e cosi' via...
    })
  })
})

I problemi principali di questo stile:

  1. Asynchronous spaghetti — la computazione complessiva viene frammentata in handler asincroni sparsi e non strutturati, compromettendo la modularità e la struttura del programma.
  2. Scarsa leggibilità — il flusso di controllo è difficile da seguire: per capirlo bisogna spostarsi a destra, giù, e poi tornare su.
  3. Scarsa riusabilità ed estendibilità — le callback annidate sono strettamente legate al contesto di annidamento, difficili da estrarre e testare.

Un esempio concreto mostrato dal professore è quello del server HTTP in Node.js, dove l'annidamento è ancora più pronunciato. Esploratelo riga per riga:

Il professore cita un commento emblematico di uno sviluppatore sul Google Group di Node.js: "I love async, but I can't code like this." La necessità di un meccanismo migliore era evidente.

Nota del redattore

Il termine "pyramid of doom" è stato coniato dalla comunità JavaScript per descrivere la forma triangolare che assume il codice quando le callback si annidano. La citazione "I love async, but I can't code like this" riportata nelle slide proviene da un complaint sul Node.js Google Group.

Per l'esame

Fatevi un esempio concreto: pensate a un'app che deve caricare un utente, poi il suo profilo, poi la foto. Con le callback avreste tre livelli di annidamento. Dovete saper spiegare perché è un problema e come Promise e async/await lo risolvono.

7. Promise: stati e API

Le Promise, proposte originariamente nel 1976 da Daniel Friedman e D. Wise, sono la risposta al callback hell. Una Promise è un oggetto proxy che rappresenta un risultato non ancora disponibile ma che sarà calcolato in futuro (simile ai future). Incapsula azioni asincrone, comportandosi come un valore di ritorno di una computazione, anche se il valore potrebbe non essere ancora disponibile.

Proprietà fondamentali

Proprietà chiave

L'immutabilità dopo la risoluzione è la proprietà che permette ragionamenti deterministici sul codice asincrono.

Esplora i tre stati con il widget interattivo qui sotto:

L'API fondamentale: myPromise.then(onComplete, onError) dove onComplete è la callback chiamata se/quando la funzione asincrona termina correttamente, e onError (opzionale) è la callback chiamata in caso di errore.

let promisedPic = loadUserPic('john');
promisedPic.then(
  (pic) => { ui.show(pic); },
  (err) => { console.error(err); }
);

Creazione di una Promise

// Creazione di una Promise
let promise = new Promise((resolve, reject) => {
    // fai qualcosa di asincrono...
    if (/* tutto ok */) {
        resolve("Stuff worked!");
    } else {
        reject(Error("It broke"));
    }
});

// Uso con then
promise.then((result) => {
    console.log(result); // "Stuff worked!"
}, (err) => {
    console.log(err); // Error: "It broke"
});

Un esempio più realistico dalle slide, con successo o fallimento casuale:

function delayWithRandom(t, p) {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            let r = Math.random();
            if (r > p) {
                resolve(r);   // successo
            } else {
                reject(r);    // fallimento
            }
        }, t);
    });
    return promise;
}

let promise = delayWithRandom(1000, 0.5);
promise.then(
    (r) => { console.log("successo:", r); },
    (r) => { console.log("fallimento:", r); }
);

Il costruttore new Promise((resolve, reject) => { ... }) riceve una funzione executor che avvia subito il lavoro asincrono. Le funzioni resolve e reject sono i callback che, quando chiamati, portano la promise nello stato rispettivo. Lo stesso esempio, annotato riga per riga:

Idea chiave

Le Promise permettono di appiattire l'annidamento delle callback. Invece di passare una callback dentro l'altra (pyramid), si incatenano i .then() uno dopo l'altro in sequenza lineare. Questo è il vantaggio fondamentale rispetto al CPS puro.

Configurazione vs esecuzione

Il professore sottolinea un punto cruciale: quando chiamate promise.then(callback), non state eseguendo nulla. Siete in una fase di configurazione: state dichiarando cosa deve succedere quando l'evento si verifica. È la stessa idea del modello dichiarativo che ritroveremo nella programmazione reattiva.

Configurazione vs Esecuzione

Il then non esegue la callback: la registra. È una fase di configurazione in cui specificate in modo dichiarativo il workflow che deve svolgersi quando la Promise viene risolta. Questa distinzione è fondamentale e ritorna in tutto il paradigma asincrono e reattivo.

Promisificazione di codice esistente

Il professore mostra come "promisificare" una chiamata AJAX tradizionale, incapsulando l'API callback-based in una Promise:

function get(url) {
  // Restituisce una nuova Promise
  return new Promise((resolve, reject) => {
    let req = new XMLHttpRequest();
    req.open('GET', url);
    req.onload = () => {
      if (req.status == 200) {
        resolve(req.response);
      } else {
        reject(Error(req.statusText));
      }
    };
    req.onerror = () => {
      reject(Error("Network Error"));
    };
    req.send();
  });
}

// Uso
get('story.json').then(
  (response) => { console.log("Success!", response); },
  (error) => { console.error("Failed!", error); }
);

8. Promise chaining e composizione

La caratteristica che rende le Promise così potenti è che il metodo then restituisce a sua volta una Promise, permettendo di creare catene (chaining) che appiattiscono la pyramid of doom in una sequenza lineare:

let promise = new Promise((resolve, reject) => {
  resolve(1);
});
promise.then((val) => {
  console.log(val); // 1
  return val + 2;
}).then((val) => {
  console.log(val); // 3
});

Il confronto diretto con le callback annidate rende evidente il guadagno:

// Versione con callback annidate
findUserById('john', (user) => {
  findPic(user.picId, (pic) => {
    ui.show(pic);
  });
});

// Stessa logica con Promise chaining
findUserById('john')
  .then((user) => {
    return findPic(user.picId);
  })
  .then((pic) => {
    ui.show(pic);
  });

Il codice diventa lineare e leggibile. Ogni .then() restituisce una nuova Promise, e il valore restituito dalla callback diventa il valore di risoluzione della Promise successiva. In forma compatta, con gestione dell'errore in coda:

findUserById('john')
    .then(user => findPic(user.picId))
    .then(pic => ui.show(pic))
    .catch(err => console.error(err));

Promise.all([delayWithRand(1000), delayWithRand(2000)])
    .then(([r1, r2]) => console.log(r1, r2));
Nota del redattore

La possibilità di restituire una Promise da un then permette di appiattire la computazione asincrona: invece di annidare callback, si restituisce una nuova Promise che sarà risolta quando l'operazione asincrona successiva terminerà. È qui che si vede la differenza tra CPS e Promise: nel CPS si annida, nella Promise si incatena linearmente.

Propagazione dei valori

Propagazione dei valori

Il valore di ogni Promise in una catena è il valore restituito dalla precedente callback onComplete/onError. Per incatenare funzioni asincrone (non solo la prima che genera la Promise), si restituiscono Promise nella catena.

let myProm1 = delayWithRandom(1000, 0.5);
let myProm2 = myProm1.then((v) => {
  console.log(v);
  return "done";
});
myProm2.then((v) => {
  console.log(v); // Stampa "done"
});

Un esempio completo di catena di operazioni asincrone dalle slide:

// Promise chaining: la piramide diventa sequenza
function delayWithRand(t) {
    return new Promise((resolve) => {
        setTimeout(() => { resolve(Math.random()); }, t);
    });
}

let promise = delayWithRand(1000);
promise
    .then((r) => {
        console.log("successo:", r);
        return delayWithRand(1500);
    })
    .then((r) => {
        console.log("successo:", r);
        return delayWithRand(500);
    })
    .then((v) => {
        console.log(v);
    });

Pattern di flusso

Le Promise supportano pattern fondamentali di flusso asincrono:

PatternCodiceDescrizione
SequenzialeasyncFunc1().then(() => asyncFunc2()).then(() => asyncFunc3())Ogni operazione asincrona attende la precedente.
ParalleloPromise.all([f1(), f2()])Operazioni indipendenti eseguite concorrentemente, con join point quando tutte completano.
RacePromise.race([f1(), f2()])Come all, ma completa quando la prima operazione termina (risolta o rifiutata).

Il parallelismo si può ottenere anche attaccando più then alla stessa Promise:

let myPromise = asyncFunc1(...);
myPromise.then((v) => { return asyncFunc2(...); });
myPromise.then((v) => { return asyncFunc3(...); });

Nel pattern parallelo, entrambe le callback sono attaccate alla stessa Promise: vengono chiamate entrambe quando asyncFunc1 termina, con lo stesso valore risultato. Le due funzioni asincrone (asyncFunc2 e asyncFunc3) vengono quindi eseguite concorrentemente.

Esecuzione in sequenza con Promise chaining:

asyncFunc1()
    .then((v) => {
        console.log(v);
        return asyncFunc2();
    })
    .then((v) => {
        console.log(v);
        return asyncFunc3();
    })
    .then(...);

Comporre un insieme di chiamate asincrone indipendenti da eseguire in parallelo, con un punto di ricongiungimento (join):

let p1 = delayWithRand(1000);
let p2 = delayWithRand(2000);
Promise.all([p1, p2]).then((values) => {
    console.log(values[0]);
    console.log(values[1]);
});

Il callback viene chiamato quando tutte le Promise sono risolte o almeno una è rigettata.

Comporre un insieme di chiamate asincrone completando al primo risultato:

let p1 = delayWithRand(1000);
let p2 = delayWithRand(2000);
Promise.race([p1, p2]).then((value) => {
    console.log(value);
});

Il callback viene chiamato quando la prima Promise viene risolta o rigettata.

Promise.all e Promise.race in dettaglio

MetodoComportamentoEsempio d'uso
Promise.all([...])Attende che tutte le Promise siano risolte (o una sia rigettata). Le callback ricevono un array con i valori.Eseguire più richieste indipendenti in parallelo e attendere tutte le risposte. È l'equivalente di un join point nella programmazione concorrente tradizionale.
Promise.race([...])Completa non appena una qualsiasi Promise si risolve o viene rigettata.Timeout su una richiesta: chi arriva prima tra richiesta e timer; oppure prendere il primo risultato disponibile.
// Promise.all - join point per task paralleli
let myProm1 = delayWithRand(1000);
let myProm2 = delayWithRand(2000);
let myTotalProm = Promise.all([myProm1, myProm2]);
myTotalProm.then((values) => {
  console.log(values[0]); // risultato del primo
  console.log(values[1]); // risultato del secondo
});

// Promise.race - chi arriva prima
let myProm3 = delayWithRand(1000);
let myProm4 = delayWithRand(2000);
let myRaceProm = Promise.race([myProm3, myProm4]);
myRaceProm.then((value) => {
  console.log(value); // risultato del piu' veloce
});
Per l'esame

Ricordate la differenza tra Promise.all e Promise.race: all attende tutte le Promise (fallisce se una fallisce), race termina appena la prima Promise si risolve o viene rifiutata. all restituisce un array con tutti i valori, race restituisce un singolo valore.

Gestione degli errori

Le Promise migliorano significativamente la gestione degli errori rispetto alle callback pure. Le regole sono chiare:

asyncTask()
  .then((result) => { return transform(result); })
  .catch((err) => { console.error("Errore:", err); })
  .then((final) => { console.log("OK:", final); });

Visualizza il flusso di una catena con lo stepper:

9. Limiti delle Promise

Nonostante i vantaggi, le Promise presentano alcuni limiti significativi che il professore analizza nel dettaglio.

Problemi noti

Eagerness e il parametro di then

Considerate questo codice apparentemente corretto ma in realtà sbagliato:

printTime();
delay(1000)
  .then(printTime)
  .then(delay(1000))   // PROBLEMA!
  .then(printTime);

Il problema è che delay(1000) viene invocato immediatamente, non passato come funzione da chiamare. Il risultato corretto è:

printTime();
delay(1000)
  .then(printTime)
  .then(() => { return delay(1000); })
  .then(printTime);
Attenzione

È facilissimo cadere in questo errore: passare il risultato dell'invocazione di una funzione invece di passare la funzione stessa. La differenza tra .then(delay(1000)) e .then(() => delay(1000)) è sottile ma cruciale. Nel primo caso la Promise viene creata subito (eagerness), nel secondo viene creata solo quando serve.

Impossibilità di usare loop/iterazioni

for (let i = 0; i < 3; i++){
  delay(1000).then(() => {
    let time = new Date().getTime();
    console.log(i + " > " + time);
  })
}
// NON funziona come previsto: tutte e tre partono concorrentemente!

Con un for classico, tutte le Promise vengono create nello stesso ciclo dell'event loop: partono quindi concorrentemente, non in sequenza. Il professore usa un esempio concreto: quando chiamate delay(1000) in un for, create 10 Promise che partono tutte nello stesso momento (stesso "ciclo" dell'event loop). Se volevate aspettare 1 secondo tra una stampa e l'altra, il risultato è sbagliato: tutte le stampe arrivano dopo un secondo, non ogni secondo per dieci secondi.

Per ottenere una sequenza con le Promise, serve la ricorsione:

function sequenza(indice) {
  if (indice >= 3) return;
  delay(1000).then(() => {
    console.log(new Date().getTime());
    sequenza(indice + 1);
  });
}

Il professore commenta: "Se interpreti l'aspetto funzionale in modo puro, non hai bisogno del for. Ma per dirvi come si complica."

Cancellazione

Le Promise non possono essere cancellate. Una volta avviato un task asincrono tramite Promise, non c'è modo di fermarlo. In alcuni scenari questo può portare a spreco di risorse.

10. Async/await

L'estensione linguistica async/await è la soluzione più recente e promettente nella programmazione asincrona. Introdotta in ES2017 (8ª edizione dello standard ECMAScript, giugno 2017), è implementata in .NET TAP (C#), Python, Dart, Kotlin e molti altri linguaggi. L'obiettivo è fornire uno stile di programmazione sincrono mantenendo un core asincrono: il codice asincrono appare e si comporta come codice sincrono, ma senza blocco.

L'idea fondamentale:

// Stile async con Promise
let p = asyncFunc(...);
p.then(onCompleteFunc, onErrorFunc)

// Stile async/await - sembra sincrono ma e' asincrono
let res = await asyncFunc(...);
// usa res -> qui arrivo solo quando la Promise e' risolta

L'operatore await si applica a qualsiasi Promise e sospende l'esecuzione della funzione finché la Promise non è risolta, ma non blocca il thread di controllo: il controllo viene ceduto (yield), salvando lo stato corrente della computazione. await converte una Promise in un risultato o un'eccezione, esattamente come se fosse il risultato di una funzione sincrona.

"Return riprendibile"

await si comporta come un "return riprendibile": quando una funzione esegue await, restituisce al chiamante una Promise in stato pending, salvando lo stato della computazione. Quando la Promise si risolve, la funzione viene ripresa dall'event loop al prossimo ciclo disponibile, come se fosse un evento. Sotto il cofano, questo meccanismo sfrutta le coroutine.

Regole fondamentali

Attenzione

await può essere usato solo all'interno di funzioni marcate come async. Non è utilizzabile al livello top-level del programma: serve introdurre almeno una funzione main asincrona.

var myFunc = async function() {
  console.log("here1");
  await delay(3000);
  console.log("here2");
  await delay(2000);
  console.log("here3");
  return 1 + 2;
};

Una funzione async è una funzione sospendibile che restituisce una Promise. Il corpo viene convertito in un oggetto AsyncFunction che contiene il macchinario di basso livello (basato su coroutine) per esprimere il corpo usando uno stile sincrono.

console.log("pre");
console.log(myFunc);      // [AsyncFunction]
console.log(myFunc());    // Promise {}
console.log("post");
// Output:
// pre
// [AsyncFunction]
// Promise {}
// post

Segui l'esecuzione riga per riga con l'annotated code:

Funzioni async con e senza await

Il professore mostra una differenza importante: chiamare una funzione async con o senza await produce comportamenti diversi.

async function pause(t) {
    console.log("before");
    let promise = new Promise((resolve) => {
        setTimeout(() => { resolve(); }, t);
    });
    await promise;
    console.log("after"); // eseguito dopo il timer
}
console.log("before call");
pause(1000); // <-- senza await qui
console.log("after call");
// Output: before call, before, after call, after

La funzione parte, stampa "before", await cede il controllo, e torna subito al chiamante che stampa "after call". Solo dopo il timer, "after" viene stampato.

async function main() {
    console.log("before call");
    await pause(1000); // <-- con await qui
    console.log("after call");
}
main();
// Output: before call, before, after, after call

Qui await pause(1000) aspetta che tutta la funzione pause sia completata. "after call" viene stampato solo dopo che pause ha terminato.

Benefici

Vantaggi
async function main() {
    console.log("Before");
    for (let i = 0; i < 3; i++) {
        await waitFor(1000);
        console.log("Step " + i);
    }
    console.log("After");
}
main();

L'esempio del ciclo, annotato riga per riga:

Simula l'esecuzione sequenziale del ciclo con lo stepper:

Problemi ancora aperti

ProblemaDescrizione
await solo in asyncNon utilizzabile al livello top-level, serve una funzione async wrapper.
Event-loop semantics preservedUna funzione async non può riprendersi mentre il thread è occupato ad eseguire altri handler.
Blocchi {…} non atomiciL'esecuzione del codice di un singolo blocco può estendersi su più iterazioni dell'event loop, aprendo a race condition.
ComposizioneServe mescolare async/await con l'API Promise per composizioni complesse (es. Promise.all).
Design clash sincrono/asincronoMescolare stili sincrono e asincrono richiede forte disciplina per evitare comportamenti difficili da comprendere.
Race condition con async/await

Il professore mette in guardia: con async/await i blocchi non sono più atomici. Se avete una variabile condivisa modificata in due punti diversi di uno stesso blocco async con un await in mezzo, tra le due modifiche l'event loop può eseguire altro codice che accede alla stessa variabile. È qui che nascono le race condition in un modello altrimenti single-threaded.

Modularità e incapsulamento

Un problema ulteriore è di modularità/incapsulamento: una funzione async chiamata senza await produce un comportamento diverso, e il chiamante deve sapere se la funzione è asincrona o meno — un buco nell'incapsulamento. Un esempio discusso dal professore mostra come sia facile sbagliare se non si rispetta la "catena" async:

// Questo NON funziona come ci si aspetterebbe
function g() {
  console.log("started g");
  f(); // f e' async, ma g non la awaita
  console.log("done g");
}

// Versione corretta
async function g() {
  console.log("started g");
  await f();
  console.log("done g");
}

Il design clash

Un aspetto critico sottolineato dal professore è il design clash tra stile sincrono e asincrono: serve una forte disciplina nel mescolare i due stili per evitare programmi con comportamento difficile da comprendere. C'è bisogno di astrazioni di livello più alto che integrino naturalmente prospettiva sincrona e asincrona — è qui che si inserisce la programmazione reattiva.

Per l'esame

Un errore comune è dimenticare di fare await di una funzione async. Senza await, la funzione parte ma il controllo prosegue immediatamente, portando a comportamenti non deterministici. Il professore lo chiama il "design clash" tra stile sincrono e asincrono: serve disciplina per non mescolarli in modo scorretto.

11. Coroutine e fiber

Sia async/await che i thread leggeri condividono un meccanismo di base: le coroutine. Le coroutine sono una generalizzazione del concetto di subroutine che permette all'esecuzione di essere sospesa e ripresa (Conway, 1963, per la compilazione di diagrammi di transizione). Mentre una subroutine tradizionale ha un solo punto di ingresso e un solo punto di uscita (il return), una coroutine può avere più punti di sospensione (yield o await) e può essere ripresa dal punto in cui si era fermata, senza perdere lo stato locale.

Storicamente le coroutine sono nate nei linguaggi assembly, poi in Simula, Smalltalk, Modula-2. Recentemente hanno ricevuto rinnovata attenzione proprio grazie alla programmazione asincrona (Ruby, Lua, Julia, Go, Kotlin). Non sono legate agli eventi o alla programmazione event-driven: sono un meccanismo più fondamentale di controllo del flusso.

flowchart TB
    subgraph Subroutine
        A1["call X()"] --> A2["esegui corpo"]
        A2 --> A3["return al chiamante"]
    end
    subgraph Coroutine
        B1["call coroutine()"] --> B2["esegui fino a yield"]
        B2 -->|"yield"| B3["sospendi e cedi controllo"]
        B3 -->|"resume"| B4["riprendi dal punto di sospensione"]
        B4 --> B2
    end
  

Differenza tra subroutine (in alto) e coroutine (in basso): la subroutine torna sempre al chiamante, mentre la coroutine può sospendersi e riprendere l'esecuzione dal punto in cui si era fermata.

Caratteristiche principali

Coroutine come routine generalizzate

Quando si chiama una coroutine, essa può uscire chiamando altre coroutine, che possono successivamente tornare al punto in cui erano state invocate nella coroutine originale. Dal punto di vista della coroutine, non sta uscendo ma chiamando un'altra coroutine — e lo fa tramite yield. Una coroutine mantiene lo stato tra le invocazioni e possono esistere più istanze della stessa coroutine simultaneamente.

Idea chiave

Le coroutine sono un meccanismo di basso livello per implementare cooperative multitasking (non preemptive). Sono utilizzate come base per: cooperative tasks, eccezioni, event loop, iteratori, liste infinite, pipe. Non sono direttamente correlate all'event-driven programming.

Esempio classico: sistema produttore-consumatore cooperativo a singolo flusso, dove produce e consume si alternano tramite yield.

// Esempio: producer-consumer cooperativo con coroutine
var q := new queue

coroutine produce
  loop
    while q is not full
      create some new items
      add the items to q
    yield to consume

coroutine consume
  loop
    while q is not empty
      remove some items from q
      use the items
    yield to produce
flowchart LR
      subgraph "Coroutine Producer"
        P1[crea item] --> P2[yield to Consumer]
        P2 --> P1
      end
      subgraph "Coroutine Consumer"
        C1[prende item] --> C2[yield to Producer]
        C2 --> C1
      end

Modello di concorrenza basato su coroutine

Le coroutine possono essere usate come meccanismo di basso livello per implementare thread leggeri (lightweight threads): un thread può avere molte coroutine al suo interno, ma solo una coroutine è in esecuzione in un dato momento. Sono veloci ed economiche da creare e gestire, non richiedendo context switching da parte del sistema operativo.

Per l'esame

Il Prof. Ricci fa notare che le coroutine non sono un mapping 1:1 con i thread: molte coroutine possono vivere su un singolo thread. Questo è il segreto dell'efficienza di Go (goroutine) e Kotlin (coroutine). La BEAM di Erlang funziona allo stesso modo: migliaia di processi logici su pochi thread OS.

Coroutine in Kotlin

Kotlin è un esempio moderno di linguaggio con supporto nativo alle coroutine. Concetti principali:

ConcettoDescrizione
Suspending computationsComputazioni che possono sospendere la loro esecuzione senza bloccare il thread in cui risiedono, permettendo al thread di essere usato per altre computazioni.
Coroutine dispatchersDeterminano su quale thread avviare o riprendere una coroutine.
Coroutine buildersasync() (quando serve un risultato), launch() (senza risultato), runBlocking() (ponte tra codice bloccante e suspendable).

Esempio Kotlin discusso a lezione:

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("before async call")
    val result = async {
        println("inside the async call")
        delay(1000)
        println("exiting the async call")
        100
    }
    println("after the async call, before greet")
    greetDelayed(200)
    println("after greet trigger")
    println("${result.await()}")
}

suspend fun greetDelayed(delayMillis: Long) {
    delay(delayMillis)
    println("Hello, World!")
}

L'output mostra l'alternanza tra le due coroutine:

before async call
after the async call, before greet
inside the async call
Hello, World!
after greet trigger
exiting the async call
100

La funzione greetDelayed è marcata come suspend: può sospendersi senza bloccare il thread, permettendo al thread di essere usato per altre computazioni. Il delay è una funzione suspending che sospende l'esecuzione senza bloccare il thread sottostante.

Fiber (fibre)

Una fiber (fibra) è un thread leggero di esecuzione. Come i thread, condivide lo spazio di indirizzi; a differenza dei thread, usa la cooperazione invece del preemptive multitasking. Le fibre possono essere implementate usando un singolo thread. Implementate da UNIX/PThreads, Windows/.NET e, più recentemente, dai Virtual Threads di Java (da JDK 19).

Coroutine vs Fiber

Le coroutine sono un costrutto a livello di linguaggio (una forma di controllo del flusso), mentre le fiber sono un costrutto a livello di sistema (thread che non eseguono in parallelo). Le fiber possono essere viste come un'implementazione delle coroutine o come substrato su cui implementare coroutine.

Nota del redattore

Il professor Ricci cita Brian Goetz (2022): i virtual threads forniscono una migliore modularità e incapsulamento per processi logici e flussi di controllo rispetto all'async programming. Tuttavia, come i thread fisici, non offrono un trattamento first-class per eventi e computazioni reattive, che devono essere implementati e gestiti a livello applicativo.

Per l'esame

Il dibattito tra virtual threads e async/await è ancora aperto. I virtual threads offrono un modello più semplice (un thread per task, senza dover riscrivere il codice in stile asincrono), ma non risolvono il problema della gestione degli eventi e della reattività, che richiede comunque astrazioni aggiuntive a livello applicativo.

Nota del redattore

Kotlin è un esempio moderno di linguaggio che integra le coroutine come costrutto di primo livello, con dispatcher, builder (async, launch, runBlocking) e funzioni suspending. Vedi slide 63-68 del modulo 2.1 per i dettagli.

12. Platform thread e virtual thread in Java

Il docente introduce un tema importante per la programmazione concorrente moderna in Java: la differenza tra thread tradizionali e virtual thread, introdotti come feature preview nelle versioni recenti di Java (Project Loom) e stabili da JDK 21. I Virtual Thread rappresentano il tentativo di Java di portare le fiber nel mondo mainstream, mantenendo la compatibilità con l'API java.lang.Thread esistente.

I platform thread (chiamati anche thread nativi o OS thread) sono direttamente associati a thread del sistema operativo. Ogni thread Java corrisponde a un thread del SO (su Linux, Windows, macOS).

  • Sono «pesanti»: la creazione e il context switch hanno costo significativo.
  • Il numero massimo è limitato dalla memoria e dalle risorse del kernel.
  • Ogni context switch richiede una chiamata di sistema: il kernel sospende un thread e ne ripristina un altro.
  • Su sistemi moderni, creare decine di migliaia di platform thread non è praticabile.

I virtual thread (Project Loom, Java 21+) sono thread logici gestiti interamente dalla JVM, non dal sistema operativo.

  • Sono «leggerissimi»: si possono creare centinaia di migliaia o milioni di virtual thread.
  • La JVM monta e smonta i virtual thread su un pool di platform thread (tipicamente pari al numero di core).
  • Non c'è context switch a livello OS: il passaggio da un virtual thread all'altro è molto più economico.
  • La stessa API di Thread viene riutilizzata: la creazione avviene con Thread.ofVirtual().
Idea chiave

I virtual thread separano il modello di concorrenza (logico, orientato al problema) dall'implementazione fisica (thread OS). Permettono di strutturare un programma con centinaia di migliaia di entità concorrenti senza preoccuparsi del numero di core disponibili.

Il docente sottolinea che questa distinzione permette di usare un approccio orientato al problema e al dominio, senza dover ottimizzare manualmente il numero di thread in base all'hardware sottostante.

Mounting e unmounting

L'idea è quella di disaccoppiare il thread logico (il "filo" di esecuzione) dal thread fisico del sistema operativo. I virtual thread sono gestiti interamente dalla JVM, che li monta e smonta sui thread fisici (detto mounting/unmounting) in modo trasparente. Quando un virtual thread esegue un'operazione bloccante (es. I/O, wait(), join()), la JVM lo sospende e riutilizza il thread fisico per un altro virtual thread.

// Creazione di un Virtual Thread (esempio dalle slide del Lab)
Thread.ofVirtual()
    .name("my-virtual-thread")
    .start(() -> {
        System.out.println("Ciao da un virtual thread!");
    });

// Creare 100.000 virtual thread e' possibile e leggero
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        executor.submit(() -> {
            // computazione leggera
        });
    }
}
Idea chiave

La differenza fondamentale con async/await: con i virtual thread, non serve marcare esplicitamente i punti di sospensione. Ogni chiamata bloccante diventa automaticamente un punto di unmounting/remounting. Il codice rimane sincrono tradizionale, ma il thread sottostante non viene mai bloccato. Il professore nota che questo è simile a quanto fa C# con i suoi task, ma in Java è più trasparente: non serve await esplicito.

Dal punto di vista architetturale, i virtual thread non forniscono un trattamento first-class per eventi e computazioni reattive — questo è il punto del dibattito tuttora aperto tra virtual thread e async programming (Brian Goetz, 2022). I virtual thread risolvono il problema del costo dei thread, ma non offrono un modello per gestire eventi e reattività.

Verifica le tue conoscenze

Cosa stabilisce la "never-blocking rule" nell'event loop?

Gli event handler non devono mai contenere chiamate bloccanti (I/O, attese) e devono sempre terminare. Una chiamata bloccante blocca l'intero event loop, impedendo il processamento di tutti gli eventi successivi. Le operazioni bloccanti devono essere sostituite da richieste asincrone servite da thread esterni che poi generano un evento nella coda.

Cosa dice la "never-blocking rule" e perché è importante?

Gli event handler non devono mai bloccarsi e devono sempre terminare. Se un handler si blocca, blocca l'intero event loop, impedendo il processamento di qualsiasi altro evento successivo. Le operazioni lunghe vanno delegate a thread asincroni separati (pool) che, al termine, inseriranno un evento nella coda per notificare il completamento.

Quali sono le principali differenze tra programmazione event-driven e programmazione con thread tradizionali?

Nell'event-driven programming c'è un singolo thread (event loop) che esegue handler atomici senza concorrenza. Non ci sono race condition a basso livello (nessuno stato condiviso). I thread tradizionali usano invece multitasking preemptive con memoria condivisa, che introduce il rischio di interferenze, deadlock, e race condition. L'event loop segue la regola del never-block e delega le operazioni bloccanti a worker thread in background.

Cosa si intende per "Continuation Passing Style" (CPS) e qual è il suo problema principale?

Il CPS è uno stile di programmazione dove il controllo viene passato esplicitamente sotto forma di continuazione (callback). La callback viene invocata dall'event loop quando il risultato asincrono è pronto. Il problema principale è il callback hell (Pyramid of Doom): l'annidamento delle callback porta a codice illeggibile, difficile da mantenere, estendere e riutilizzare. Le Promise risolvono questo problema appiattendo l'annidamento grazie al chaining con .then().

Quali sono i tre stati di una Promise?

Pending — appena creata, il risultato non è ancora disponibile. Resolved (fulfilled) — l'operazione asincrona è completata con successo, il valore è disponibile. Rejected — l'operazione è fallita con un errore. Una volta che una promise è settled (resolved o rejected), il suo stato e valore sono immutabili.

Come funziona Promise.all? In che caso è preferibile a Promise.race?

Promise.all prende un array di Promise e restituisce una nuova Promise che si risolve quando tutte le Promise nell'array sono risolte, oppure si rifiuta appena una di esse si rifiuta. Restituisce un array con tutti i valori. È ideale quando si devono eseguire operazioni asincrone indipendenti in parallelo e serve attendere che tutte completino (pattern fork-join, join point). Promise.race, invece, si risolve o rifiuta appena la prima Promise si risolve/rifiuta, restituendo un singolo valore.

Perché il seguente codice non funziona come previsto?
for (let i=0; i<3; i++) { delay(1000).then(() => print(i)); }

Tutte e tre le Promise vengono create nello stesso ciclo dell'event loop, quindi partono concorrentemente (non in sequenza). Dopo 1 secondo, le tre callback vengono eseguite quasi simultaneamente. Il risultato non è una sequenza di stampe distanziate di 1 secondo, ma tre stampe ravvicinate dopo 1 secondo. Per ottenere la sequenza serve ricorsione o async/await.

Quali sono i tre limiti principali delle Promise discussi a lezione?

1) Eagerness e problema del parametro di then: .then(delay(1000)) invoca immediatamente invece di passare la funzione (serve lambda). 2) Impossibilità di usare loop/iterazioni: il for crea tutte le Promise nello stesso ciclo (serve ricorsione). 3) Impossibilità di cancellazione: una Promise non può essere cancellata una volta avviata. Tutti e tre sono risolti da async/await.

Qual è il problema principale delle promise (eager) e come lo risolve async/await?

Le promise sono eager: appena costruite iniziano subito il lavoro. Inoltre, con .then() non si possono passare parametri facilmente alle funzioni chiamate. Async/await risolve entrambi i problemi: l'avvio è controllato dalla chiamata esplicita della funzione async, e i parametri si passano normalmente come in una chiamata sincrona.

Quali sono le criticità di async/await evidenziate dal professore?

1) await può essere usato solo dentro funzioni async, non al livello top-level. 2) Una funzione async non può riprendersi finché il thread è occupato con altri handler (la semantica dell'event loop rimane). 3) I blocchi {...} non sono più garantiti atomici: l'esecuzione di un singolo blocco può estendersi su più iterazioni dell'event loop, aprendo a race condition. 4) C'è un "design clash" tra stile sincrono e asincrono che richiede forte disciplina per evitare codice difficile da comprendere.

Spiega brevemente la differenza tra coroutine e fiber.

Le coroutine sono un costrutto a livello di linguaggio: una forma di controllo del flusso che permette sospensione e ripresa. Le fiber sono un costrutto a livello di sistema: thread leggeri che condividono lo spazio di indirizzi e usano cooperazione (non preemption). Le fiber possono implementare coroutine, o essere il substrato su cui le coroutine sono costruite.

Come si differenziano i virtual thread dai thread platform tradizionali?

I virtual thread sono gestiti interamente dalla JVM (non dal sistema operativo), sono molto più leggeri (se ne possono creare centinaia di migliaia), e il loro mounting/unmounting sui thread fisici è automatico e trasparente. Rispetto ad async/await, non serve marcare esplicitamente i punti di sospensione: ogni operazione bloccante diventa automaticamente un punto di unmounting.

Qual è la differenza fondamentale tra thread e attori per la comunicazione?

Nei thread la comunicazione avviene tramite memoria condivisa (con variabili mutabili, race condition, lock, synchronized). Negli attori la comunicazione avviene esclusivamente tramite messaggi asincroni (message passing). Lo stato di un attore non è mai accessibile direttamente da un altro attore.

Cosa significa "macro-step semantics" nel modello Actor?

Significa che ogni handler di messaggio viene eseguito completamente (run-to-completion) prima che l'attore possa servire il messaggio successivo. Questo evita race condition all'interno dell'attore (lo stato non viene mai interrotto a metà di un handler) ma richiede che gli handler non siano bloccanti, proprio come nell'event loop.

Qual è lo scopo di StructuredTaskScope in Java?

Strutturare un task come un nucleo di sottotask concorrenti che devono tutti tornare allo stesso blocco lessicale (il scope). Garantisce che tutti i sottotask siano completati quando il blocco termina (grazie al try-with-resources), eliminando il problema dei thread "fuggiti". Propaga automaticamente le eccezioni dal sottotask al padre.

Cosa sono la "poison pill" e lo "stashing" nel contesto degli attori?

La poison pill è un oggetto sentinella inserito nella coda dei messaggi che segnala a un attore/consumer di terminare. Lo stashing (Akka) è un meccanismo per accodare temporaneamente i messaggi ricevuti quando l'attore non è nello stato giusto per processarli, per poi riprenderli quando il comportamento cambia (es. con context.become()).

Qual è la differenza fondamentale tra una Promise e un Observable?

Una Promise rappresenta un singolo valore futuro (o errore), risolvibile una volta sola. Un Observable rappresenta un flusso di valori nel tempo, potenzialmente infinito. La Promise produce un risultato una tantum (resolved/rejected); l'Observable produce eventi OnNext multipli fino a OnCompleted o OnError. Inoltre, l'Observable supporta la cancellazione tramite dispose().

Cosa sono i glitch nella programmazione reattiva?

Sono inconsistenze temporanee che si verificano durante la propagazione del cambiamento nel modello push. Ad esempio, se var3 = var1 + var2 e var2 = var1 * 1, al cambiamento di var1 da 1 a 2 si potrebbe osservare var3 = 3 (invece di 4) se var1 + var2 viene ricalcolato prima che var2 sia aggiornato. I framework moderni implementano tecniche di glitch avoidance (topological ordering).

Qual è la differenza tra flussi hot e cold in Rx?

Un cold stream crea un nuovo flusso indipendente per ogni subscriber, partendo dall'inizio. Ogni subscriber riceve tutti gli elementi. Un hot stream esiste indipendentemente dai subscriber: quelli in ritardo ricevono solo gli eventi successivi alla sottoscrizione. Un flusso hot può emettere eventi anche senza subscriber attivi. Alcuni hot stream possono cache/replay la storia parziale o totale.

Cosa significa "backpressure" in Rx e qual è la sua analogia?

La backpressure è la capacità del consumatore di segnalare al produttore che la velocità di emissione è troppo alta. L'analogia è una catena di montaggio: se una postazione lavora più lentamente di quella a monte, segnala di rallentare il flusso. Tecnicamente, il subscriber usa il request mechanism per chiedere al massimo n elementi, trasformando il modello push in un push-pull hybrid.

Qual è la differenza fondamentale tra una rete di Petri e un diagramma di flusso tradizionale nel contesto della concorrenza?

La rete di Petri aggiunge il concetto di token, che fornisce una "fotografia runtime" dello stato in cui si trova il processo e le possibili evoluzioni. Con un token si rappresenta un flusso di controllo (un'istanza del processo), mentre con più token si rappresentano più istanze concorrenti. Inoltre, il token può anche rappresentare risorse (come permessi), unificando la modellazione di flusso di controllo e risorse di coordinazione.

Quali sono le quattro condizioni necessarie (Coffman) per il verificarsi di un deadlock?

1) Mutua esclusione: risorsa non condivisibile; 2) Hold and wait: un processo tiene risorse mentre ne richiede altre; 3) No preemption: le risorse non possono essere revocate; 4) Circular wait: esiste una catena circolare di processi che attendono risorse detenute da altri. Per evitare il deadlock, basta rimuovere una qualsiasi delle quattro condizioni. La strategia pratica più comune è assegnare un ordine totale ai lock e acquisirli sempre in quell'ordine (rompe la circular wait).

Qual è la differenza tra una variabile di condizione in un monitor e un semaforo?

Differenze chiave: wait su semaforo può non bloccare (se V > 0) mentre waitC su condition variable blocca sempre. signal su semaforo ha sempre effetto (incrementa o sblocca), mentre signalC non ha effetto se la coda è vuota. Inoltre, signalC rilascia il lock del monitor e il processo risvegliato deve attendere che il segnalante esca, mentre signal su semaforo fa riprendere immediatamente il processo sbloccato. signalC sblocca in testa alla coda FIFO, mentre signal su semaforo sblocca un processo arbitrario (nei weak semaphore).