Programmazione Concorrente e Distribuita — Prof. Alessandro Ricci, UNIBO

Programmazione Asincrona: event loop, callback, promise, async/await, coroutine e coordinate

2026-03-27171 min registrazione originale

In questa lezione

1. Reti di Petri per la concorrenza

Il professor Ricci apre la lezione ricordando che le reti di Petri sono un formalismo già introdotto in altri corsi, ma che in questo contesto vengono utilizzate con un obiettivo molto specifico: modellare sistemi di processi concorrenti per analizzarne il comportamento, verificare proprietà di safety e liveness, e progettare la coordinazione.

Una rete di Petri è composta da place (piazze, che rappresentano stati) e transizioni (che rappresentano azioni). I token forniscono una "fotografia runtime" dello stato del sistema: ogni token rappresenta un flusso di controllo, cioè un'istanza di un processo che segue lo schema di comportamento definito dalla rete.

Idea chiave

Quando modellate un processo con una rete di Petri, le piazze rappresentano lo stato del processo, il token rappresenta il suo flusso di controllo. Le transizioni sono le azioni che il processo può compiere. Una transizione scatta se per ogni arco in ingresso c'è almeno un token nel place sorgente.

La semantica delle reti di Petri è semplice ma potente: una transizione consuma un token da ogni place in ingresso e produce un token in ogni place in uscita. Due token in uno stesso place rappresentano due processi distinti con la stessa struttura comportamentale.

Il consiglio del professore è di pensare sempre a cosa si vuole modellare: se il token rappresenta un flusso di controllo, consumare un token significa "il flusso prosegue"; se invece il token rappresenta una risorsa (come un permesso), la semantica cambia. Le reti di Petri unificano entrambi i concetti in un unico formalismo.

2. Modellare il flusso di controllo

Il professore mostra come modellare costrutti di programmazione sequenziale con le reti di Petri. Consideriamo un programma che esegue A, poi valuta una condizione C e, a seconda del risultato, esegue B oppure C (if-then-else), e infine D.

La modellazione introduce una transizione che rappresenta la valutazione della condizione. Da questa partono due archi: uno verso il ramo B (condizione vera), l'altro verso il ramo C (condizione falsa). Non esiste un unico modo corretto di modellare: il livello di astrazione lo definite voi.

flowchart LR
      subgraph "Processo P"
        direction TB
        start((i)) --> A[A]
        A --> cond{C?}
        cond -->|true| B[B]
        cond -->|false| C[C]
        B --> D[D]
        C --> D[D]
        D --> loop(( ))
      end
Nota del redattore

La trascrizione originale dice: "Le azioni le modello sempre come transizione. Poi introduco una transizione che rappresenta la valutazione della condizione. C'è un solo modo? No. E questo vale per tutti i formalismi. Siete voi che definite il livello di astrazione che volete."

3. Produttori-Consumatori in reti di Petri

Il professore applica l'approccio metodologico per modellare il problema classico dei produttori e consumatori con buffer. La strategia è sempre: prima rappresentare lo schema di comportamento dei singoli processi, poi aggiungere la "colla" della coordinazione.

Il produttore è un ciclo che produce un item e lo inserisce nel bounded buffer (transizioni produce e insert). Il consumatore prende un item dal buffer (transizione get) e poi lo consuma (transizione consume). N token per N produttori, M token per M consumatori.

La coordinazione emerge introducendo place che rappresentano i permessi: un place per i posti disponibili nel buffer (inizializzato con N token) e uno per gli item disponibili (inizializzato a 0). Questi place "risorsa" sono concettualmente diversi dai place che rappresentano il flusso di controllo, ma il formalismo li unifica.

flowchart LR
      subgraph Produttore[i-esimo Producer]
        p1[produce] --> p2[insert]
        p2 --> p1
      end
      subgraph Consumatore[j-esimo Consumer]
        q1[get] --> q2[consume]
        q2 --> q1
      end
      p2 -- item --> buffer[(Buffer)]
      buffer -- item --> q1
      places[(N posti)] -- permesso --> p2
      q1 -- rilascio --> places
Idea chiave

La bellezza delle reti di Petri è che con un unico formalismo catturano sia il flusso di controllo (token = istanza del processo) sia le risorse di coordinazione (token = permesso). I place hanno lo stesso aspetto ma significato diverso a seconda del contesto.

4. Readers-Writers con reti di Petri

Il problema Readers-Writers introduce una sfida più sottile: i lettori non devono escludersi tra loro, ma devono escludere gli scrittori (e viceversa). Nella modellazione con reti di Petri, l'approccio naive che usa un singolo permesso porta a una sezione critica pura: se un lettore prende il permesso, blocca anche gli altri lettori, il che non è desiderabile.

La soluzione mostrata dal professore introduce N token nel place dei permessi per i lettori: ogni lettore consuma un token per entrare, ma se ci sono N token iniziali, fino a N lettori possono entrare simultaneamente. Finché c'è almeno un lettore attivo, gli scrittori sono bloccati.

Attenzione

Se usate un singolo token (semaforo binario) per il problema Readers-Writers, ottenete una soluzione over-constrained che serializza anche i lettori, perdendo il vantaggio del parallelismo in lettura. Il professore sottolinea: "Se consumo il token per poter entrare, il reader passa ma blocca tutti gli altri — sto serializzando i readers che non va bene."

5. Programmazione asincrona: motivazioni

Il professore apre la seconda parte della lezione (e il modulo 2.1) introducendo la programmazione asincrona come un paradigma sempre più importante e pervasivo, supportato da tutti i framework e le piattaforme moderne (Java, .NET, iOS, Android, Node.js).

La programmazione asincrona è un approccio che astrae dai thread tradizionali, focalizzandosi sulla gestione di computazioni, richieste e processi asincroni. 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.

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.

6. Event-Driven Programming e l'Event Loop

Il modello di programmazione event-driven è alla base di gran parte del software moderno: un programma è composto da routines (event handler) che vengono chiamate (eseguite, attivate) 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 ogni handler lo stack è vuoto.

L'architettura dell'Event Loop

L'event loop è il cuore del modello:

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

Un singolo thread di controllo attende gli eventi e, quando arrivano, esegue l'handler corrispondente. Gli eventi vengono accodati in una coda degli eventi, generati sia dall'ambiente sia dagli handler stessi. L'esecuzione degli handler è atomica: se un evento arriva mentre un handler è in esecuzione, viene accodato e gestito solo quando l'handler corrente termina (niente concorrenza).

Esplorate gli stati dell'Event Loop nel modello astratto:

7. Il Reactor Pattern

Il Reactor pattern (Schmidt et al., Pattern-Oriented Software Architecture Volume 2) è il pattern architetturale che formalizza l'event loop. 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.

Nota del redattore

Il Reactor pattern è alla base di framework come Node.js, Netty, Vert.x, e di gran parte dei web server moderni. La sua potenza sta nel separare la gestione degli eventi (dispatcher) dalla logica applicativa (handler).

8. La regola del never-blocking

Il modello di esecuzione dell'event loop porta a una regola fondamentale: gli event handler non devono mai bloccarsi (non devono contenere chiamate bloccanti) e devono sempre terminare (non devono contenere cicli infiniti). Questa regola garantisce la reattivita' del sistema: una chiamata bloccante o un ciclo infinito bloccherebbero l'event loop, impedendo l'elaborazione degli eventi in coda.

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."

9. Callback e Continuation Passing Style

Data la regola del never-blocking, sorge il problema centrale: come elaborare i risultati o gli errori generati asincronamente dall'esecuzione di un task/funzione asincrona? La risposta è il modello a 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).

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.

Callback come closure

Le callback sono spesso implementate come closure, una tecnica per il name binding lessicale in linguaggi con funzioni di prima classe. Una closure memorizza la funzione insieme all'ambiente (mapping delle variabili libere) presente al momento della creazione. Questo permette agli handler di accedere al contesto originale anche quando vengono eseguiti in un momento successivo.

10. Callback Hell e Pyramid of Doom

Nonostante i benefici, il CPS e la programmazione event-driven soffrono di problemi ben noti, spesso chiamati collettivamente callback hell:

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.

11. Promise: gestire l'asincronia

Le Promise, proposte originariamente nel 1976 da Daniel Friedman e D. Wise, sono un meccanismo che risolve in parte il callback hell. Una Promise è un oggetto proxy che rappresenta un risultato sconosciuto che deve ancora essere calcolato (simile ai future).

Una Promise incapsula azioni asincrone, comportandosi come un valore restituito da una computazione, solo che il valore potrebbe non essere disponibile al momento. Una proprietà chiave è che una Promise può essere risolta o rifiutata una e una sola volta (immutabile una volta risolta).

Stati di una Promise

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);
});
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.

12. 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.

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

Pattern di flusso

Le Promise supportano due 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).
Per l'esame

Ricordate la differenza tra Promise.all e Promise.race: all attendere 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.

Error handling

Le Promise migliorano significativamente la gestione degli errori rispetto alle callback pure. Le regole sono chiare: un handler che restituisce un valore e non lancia eccezioni passa al prossimo then; un handler che lancia un'eccezione fa passare la Promise allo stato rejected, propagandosi lungo la catena fino a trovare un handler di errore.

13. Async/Await

L'evoluzione più recente della programmazione asincrona è rappresentata da async/await, un'estensione del linguaggio introdotta in ES2017 (ECMAScript 8 edizione) e implementata in molti linguaggi tra cui C#, Python, Dart, Kotlin.

L'obiettivo è fornire uno stile di programmazione sincrono mantenendo un cuore asincrono. Con async/await, il codice asincrono appare e si comporta come codice sincrono, ma senza blocco.

async function waitFor(t) {
  console.log("waitFor - pre");
  await delay(t);
  console.log("waitFor - post");
}

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

L'operatore await può essere applicato a qualsiasi Promise e sospende l'esecuzione della funzione fino a quando la Promise non viene risolta. Non blocca il flusso di controllo: il controllo viene ceduto (yield), salvando lo stato corrente della computazione. L'operatore await converte una Promise in un risultato o un'eccezione, esattamente come se fosse il risultato di una funzione sincrona.

Attenzione

Il professore sottolinea alcune criticità di async/await:

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.

14. Coroutine e virtual thread

Le coroutine sono una generalizzazione del concetto di subroutine, che permette all'esecuzione di essere sospesa e ripresa (Conway, 1963). Rappresentano il meccanismo di basso livello alla base sia di async/await sia dei thread leggeri. A differenza dei thread tradizionali (preemptive), le coroutine usano scheduling cooperativo: una coroutine cede volontariamente il controllo (yield) ad un'altra.

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

L'esempio classico mostra un sistema produttore-consumatore cooperativo a singolo flusso, dove produce e consume si alternano tramite yield.

Il professore introduce anche il concetto di fiber (o virtual thread in Java, da JDK 19): un thread leggero che, come i thread, condivide lo spazio di indirizzi, ma a differenza dei thread usa cooperazione invece di preemption per il multitasking. Le fiber sono implementabili con un singolo thread OS.

La differenza fondamentale: le coroutine sono un costrutto a livello di linguaggio (controllo di flusso), mentre le fiber sono un costrutto a livello di sistema (viste come thread che non eseguono in parallelo). Il dibattito "virtual threads vs async programming" è ancora aperto, come discusso da Brian Goetz in un articolo del 2022.

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.

15. Semafori: meccanismo base

Ricollegandosi al modulo 1.3, il professore riprende i semafori come meccanismo fondamentale per la coordinazione tra processi. Introdotti da Dijkstra nel 1968, i semafori sono un costrutto semplice ma potente che permette di risolvere quasi ogni problema di mutua esclusione e sincronizzazione.

Un semaforo S è un tipo di dato composto con due campi:

Due operazioni atomiche fondamentali:

Tipi di semafori

TipoRange di VUso tipico
Mutex (binario){0, 1}Mutua esclusione, lock
Counting (risorsa)>= 0Gestione risorse multiple
EventoInizializzato a 0Sincronizzazione, segnalazione

Esiste anche la distinzione tra semafori strong (con coda FIFO, nessuno starvation) e weak (con insieme, possibile starvation). I semafori busy-wait non hanno la componente S.L e usano attesa attiva.

Sezione critica con semaforo

La soluzione del problema della sezione critica per N processi è elegante:

// qualsiasi processo i
semaphore S = (1, {}); // mutex binario

loop forever {
  p1: wait(S);
  p2: /* sezione critica */
  p3: signal(S);
  p4: /* resto */
}
Per l'esame

L'invariante del semaforo: S.V = k + #signal(S) - #wait(S) e S.V >= 0 (dove k è il valore iniziale). Questo teorema è fondamentale per la verifica formale dei programmi concorrenti con semafori.

16. Deadlock e condizioni di Coffman

Il deadlock è una situazione in cui due o più azioni in competizione attendono che le altre finiscano, e quindi nessuna procede mai. Il professore richiama le condizioni necessarie di Coffman (1971) per il verificarsi di un deadlock:

  1. Mutua esclusione — una risorsa non può essere usata da più processi contemporaneamente
  2. Hold and wait — processi che già detengono risorse possono richiederne di nuove
  3. No preemption — le risorse non possono essere rimosse forzatamente da un processo
  4. Circular wait — due o più processi formano una catena circolare di attesa

Il caso classico è il deadly embrace: thread A tiene il lock L e cerca M mentre thread B tiene M e cerca L. Nessuno dei due progredisce.

Le regole generali per evitare il deadlock: assegnare un ordine totale ai lock e acquisirli sempre nello stesso ordine. Questo rende impossibile la circular wait.

17. Monitor: astrazione per la sincronizzazione

I semafori sono potenti ma di basso livello, portando a programmi soggetti a errori e difficili da usare in programmi concorrenti complessi. I monitor, introdotti da Brinch Hansen (1973) e generalizzati da Hoare (1974), rappresentano un'astrazione di livello più alto.

Un monitor è un'astrazione dati concorrente che incapsula lo stato, le operazioni e le politiche di sincronizzazione/mutua esclusione per l'accesso. Come un modulo OOP che include il costrutto di base per garantire la correttezza dell'accesso concorrente.

Proprietà fondamentali dei monitor:

SemaforoCondition Variable (monitor)
wait può non bloccare (se V > 0)waitC blocca sempre
signal ha sempre effetto (incrementa o sblocca)signalC non ha effetto se la coda è vuota
signal sblocca un processo arbitrariosignalC sblocca in testa alla coda FIFO
Il processo sbloccato riprende subitoIl processo sbloccato deve aspettare che chi ha fatto signal esca

Tre discipline di segnalazione per il signalC:

  • Signal and Continue (E < W < S) — il segnalante continua, il risvegliato esegue dopo
  • Signal and Wait (E = S < W) — il risvegliato esegue subito, il segnalante aspetta
  • Signal and Urgent Wait (E < S < W) — come S&W ma il segnalante ha priorità su altri in attesa del lock

Dove S = processi segnalanti, W = processi in attesa sulla condizione, E = processi bloccati sull'ingresso del monitor.

18. BoundedBuffer e RWLock con monitor

Il professore presenta due casi di studio classici risolti con i monitor, mostrando come l'astrazione permetta di incapsulare in modo pulito la sincronizzazione.

BoundedBuffer con monitor

monitor BoundedBuffer {
  int[] elems := new int[MAX_ELEMS];
  int first := 0, last := 0;
  cond notFull, notEmpty;

  procedure put(int elem) {
    if ((last + 1) % MAX_ELEMS == first)
      waitC(notFull);
    elems[last] = elem;
    last := (last + 1) % MAX_ELEMS;
    signalC(notEmpty);
  }

  procedure take(): int {
    if (first == last)
      waitC(notEmpty);
    int elem = elems[first];
    first = (first + 1) % MAX_ELEMS;
    signalC(notFull);
    return elem;
  }
}

Il buffer circolare usa due variabili di condizione: notFull blocca il produttore se il buffer è pieno, notEmpty blocca il consumatore se il buffer è vuoto. signalC risveglia il processo appropriato dopo ogni operazione.

Readers-Writers con monitor

La soluzione usando un monitor con due variabili di condizione (okToRead, okToWrite):

monitor RWLock {
  int nr, nw := 0;
  cond okToRead, okToWrite;

  procedure request_read() {
    while (nw > 0) waitC(okToRead);
    nr := nr + 1;
  }

  procedure release_read() {
    nr := nr - 1;
    if (nr == 0) signalC(okToWrite);
  }

  procedure request_write() {
    while (nr > 0 or nw > 0) waitC(okToWrite);
    nw := nw + 1;
  }

  procedure release_write() {
    nw := nw - 1;
    signalC(okToWrite);
    signalAllC(okToRead);
  }
}

L'invariante fondamentale: (nr == 0 or nw == 0) and (nw <= 1). Questa soluzione garantisce che i lettori non si escludano tra loro ma escludano gli scrittori, e viceversa.

19. Verifica le conoscenze

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 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().

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.

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).

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.