Programmazione Concorrente e Distribuita — Prof. Ricci

Programmazione Concorrente e Asincrona: Introduzione e Panoramica

2026-02-1659 min registrazione originale

In questa lezione

1. Concorrenza: Definizioni e Motivazioni

La lezione si apre con una panoramica dei concetti fondamentali della programmazione concorrente. Il professor Ricci introduce il laboratorio del corso, strutturato in attivita pratiche disponibili sul sito del corso e sul repository GitHub nella sezione Lab Notes. La prima attivita riguarda una panoramica della programmazione multi-thread in Java.

Definizione

La concorrenza e una proprieta dei sistemi in cui piu processi computazionali sono in esecuzione contemporaneamente e potenzialmente interagiscono tra loro. La programmazione concorrente e la disciplina che costruisce programmi in cui multiple attivita computazionali si sovrappongono nel tempo e tipicamente interagiscono in qualche modo.

Concorrenza, Parallelismo, Programmazione Distribuita

Livello logico/astratto. La concorrenza riguarda la composizione di computazioni eseguite indipendentemente. Si concentra sull'organizzazione del programma e non presuppone necessariamente l'esecuzione su processori fisici separati. Come dice Rob Pike: "La concorrenza e un modo di strutturare il software, per scrivere codice pulito che interagisca bene con il mondo reale. Non e parallelismo: abilita il parallelismo."

Livello fisico. Il parallelismo riguarda l'esecuzione di programmi che si sovrappongono nel tempo mediante l'uso di processori fisici separati. Si concentra sulla performance, sullo speedup, sull'uso efficiente delle risorse hardware.

Quando i processori sono distribuiti su una rete e non condividono la memoria, si parla di programmazione distribuita. In questo modello non esiste memoria condivisa: la comunicazione avviene esclusivamente tramite scambio di messaggi.

Il professor Ricci sottolinea come la concorrenza sia un concetto che attraversa molti domini: sistemi operativi, programmi multi-thread e multi-processo, sistemi distribuiti, sistemi di controllo, sistemi real-time. La definizione classica di programma concorrente e quella di un insieme finito di programmi sequenziali che possono essere eseguiti in parallelo, ovvero sovrapposti nel tempo. L'esecuzione di un programma sequenziale si chiama processo; l'esecuzione di un programma concorrente si chiama computazione concorrente o elaborazione concorrente.

I paradigmi della concorrenza

La programmazione concorrente si declina in diversi paradigmi:

ParadigmaStato condivisoMeccanismi
Multi-threadedSi, memoria condivisaSemafori, monitor, synchronized
Message-basedNoScambio di messaggi asincrono/sincrono
Event-drivenVariabileEvent-loop, handler, callback
AsincronoNo (tipicamente)Future, Promise, async/await
ReactiveVariabileData flow, propagation of change

2. L'Evoluzione Hardware e la Legge di Amdahl

Il professor Ricci dedica una parte significativa della lezione all'evoluzione hardware che ha reso la programmazione concorrente non solo utile ma necessaria. Come ha scritto Herb Sutter, "The Free Lunch is Over" (SUT-12): per anni le applicazioni hanno beneficiato dell'aumento della frequenza di clock dei processori senza dover essere riscritte; oggi la crescita delle prestazioni passa attraverso l'aumento del numero di core.

Architetture multi-core

I processori moderni integrano piu core sullo stesso chip, condividendo RAM e talvolta livelli di cache. Esempi iconici includono la famiglia Intel Core i7 (2009) e l'AMD Ryzen Threadripper 3990X (2020), un processore x86 desktop a 64 core con frequenza base di 2.9 GHz e boost fino a 4.3 GHz.

Architetture ibride: P-cores ed E-cores

Le architetture recenti combinano core di diversa natura:

Un esempio e l'Intel Core i9 di 14a generazione con 24 core (fino a 32 thread): 8 P-cores a 5.8 GHz e 16 E-cores per task in background. Anche AMD con l'architettura Zen 5 adotta un approccio ibrido con chiplet design: Core Complex Dies (CCD) collegati da un fabric ad alta velocita, scalando fino a 16 core/32 thread su desktop e oltre 128 su server.

Sistemi eterogenei e many-core

Oltre ai core CPU tradizionali, i chip moderni integrano processori specializzati: GPU, NPU (neural processing unit), ISP, e vari coprocessori. L'Apple Silicon M5 ne e un esempio con SoC che include CPU, GPU, NPU e fino a 10 core CPU. Ancora piu estremi i sistemi dedicati all'AI, come il Cerebras CS-3, e i supercomputer come Fugaku (Fujitsu) con oltre 7,6 milioni di core.

Legge di Amdahl e Speedup

La legge di Amdahl e il modello matematico fondamentale per capire il limite del parallelismo. Definisce lo speedup S come rapporto tra il tempo di esecuzione sequenziale (T1) e quello parallelo con N processori (TN):

flowchart LR
    P["Parte parallelizzabile = P"] --> S["Speedup S = 1 / ((1-P) + P/N)"]
    NP["Parte sequenziale = 1-P"] --> S
    S --> R["Risultato: S tende a 1/(1-P) per N→∞"]
    

La legge afferma che lo speedup massimo e limitato dalla parte sequenziale del programma. Se il 10% del programma e intrinsecamente sequenziale, lo speedup massimo non puo superare 10, indipendentemente dal numero di processori.

Idea chiave

Si puo migliorare la performance aggiungendo processori solo per le parti che possono essere eseguite in parallelo. Le parti sequenziali hanno un impatto fortissimo sulle prestazioni complessive. Inoltre, le parti sequenziali sono spesso necessarie per la correttezza — come si vedra nei moduli successivi con i meccanismi di sincronizzazione.

Efficienza

L'efficienza E e una misura normalizzata dello speedup che indica quanto effettivamente ogni processore viene sfruttato:

E = S / N

L'efficienza ideale e 1 (tutti i processori usati a piena capacita), ma tipicamente e inferiore a causa della parte sequenziale, del costo della comunicazione e della sincronizzazione tra processori.

Attenzione

Oltre alla CPU, la memoria condivisa e il bus costituiscono un potenziale collo di bottiglia. Una sola operazione di memoria alla volta puo avvenire sul bus. Per questo la cache e fondamentale, e i protocolli di coerenza della cache sono sempre piu complessi e intelligenti.

3. Tassonomia di Flynn e Architetture MIMD

La tassonomia di Flynn classifica tutti i sistemi di calcolo in base al numero di flussi di istruzioni e di dati:

ClassificazioneFlusso IstruzioniFlusso DatiEsempi
SISDSingoloSingoloModello Von Neumann, processori single-core
SIMDSingoloMultiploProcessori vettoriali, GPU
MISDMultiploSingoloNessun sistema noto di uso comune
MIMDMultiploMultiploLa stragrande maggioranza dei sistemi moderni

Architetture MIMD

La categoria MIMD si decompone ulteriormente in base all'organizzazione della memoria:

Tutti i processi/processori condividono un unico spazio di indirizzi e comunicano scrivendo e leggendo variabili condivise. Sotto-classi:

  • SMP (Symmetric Multi-Processing): tutti i processori condividono la connessione a una memoria comune e accedono a tutte le locazioni con la stessa velocita.
  • NUMA (Non-Uniform Memory Access): la memoria e condivisa, ma blocchi di memoria possono essere fisicamente piu vicini ad alcuni processori, causando tempi di accesso diversi.

Ogni processo/processore ha il proprio spazio di indirizzi. Comunicano tramite scambio di messaggi (inviare e ricevere messaggi). Sotto-classi:

  • MPP (Massively Parallel Processors): processori e infrastruttura di rete sono strettamente accoppiati per calcolo parallelo (HPC). Altamente scalabili, migliaia di processori.
  • Cluster: sistemi a memoria distribuita composti da computer commodity collegati da rete commodity (es. Beowulf cluster su Linux).
  • Grid: risorse distribuite ed eterogenee collegate da LAN/WAN, senza un punto di amministrazione comune.

Cloud Computing

Il cloud computing consegna le risorse di calcolo come servizio attraverso la rete. Si articola in modelli XaaS: Software as a Service (SaaS), Platform as a Service (PaaS), Infrastructure as a Service (IaaS). Esempi includono Amazon EC2, Microsoft Azure, Google App Engine.

La classifica TOP500 (aggiornata a novembre 2025) mostra sistemi che utilizzano prevalentemente kernel Linux e architetture eterogenee sempre piu potenti.

4. Programmazione Concorrente come Paradigma

Oltre alle motivazioni di performance, la programmazione concorrente e importante come strumento di progettazione e costruzione del software. Il professor Ricci sottolinea come gli oggetti della OOP non siano sufficienti per programmi che interagiscono con l'ambiente, controllano molteplici attivita e gestiscono eventi multipli.

Prospettiva ingegneristica

La concorrenza ci costringe a ripensare al modo in cui risolviamo i problemi (algoritmi e strutture dati di base) e al modo in cui progettiamo e costruiamo sistemi. Introduce un nuovo livello di astrazione, con diverse forme di scomposizione, modularizzazione, incapsulamento. Impatta l'intero spettro ingegneristico: modellazione, progettazione, implementazione, verifica, testing.

Il professor Ricci introduce quindi i paradigmi della concorrenza gia accennati nella sezione 1, approfondendo come ciascuno offra una diversa lente per pensare ai problemi.

Un riferimento chiave citato e il talk di Rob Pike "Go Concurrency Patterns": il mondo e visto come un insieme di agenti che interagiscono, e questo non e catturato dai paradigmi di programmazione sequenziale. La concorrenza e un modo di strutturare il software per scrivere codice pulito che interagisca bene con il mondo reale.

5. Processi e Interazione: Cooperazione, Competizione, Interferenze

Un processo e l'unita base di un sistema concorrente: un singolo flusso di controllo (thread of control), una sequenza di istruzioni che opera come gruppo. E un concetto astratto, non necessariamente legato ai processi del sistema operativo. Caratteristiche importanti: speed independence (l'esecuzione e asincrona, non si possono fare assunzioni sulle velocita relative) e non-determinismo.

In ogni programma concorrente non banale, i processi devono interagire. Le interazioni si dividono in tre categorie fondamentali:

Interazioni attese e desiderate, parte della semantica del programma concorrente. Due forme principali:

  • Comunicazione: flusso informativo tra processi, realizzato tramite messaggi.
  • Sincronizzazione: relazioni temporali o dipendenze tra processi o tra azioni di processi distinti.

Interazioni attese e necessarie, ma non desiderate. Riguardano il coordinamento dell'accesso a risorse condivise. Due classi di problemi:

  • Mutua esclusione: regolamentare l'accesso a risorse condivise da parte di processi distinti (sezioni critiche).
  • Sezioni critiche: regolamentare l'esecuzione concorrente di blocchi di azioni da parte di processi distinti.

Interazioni ne attese ne desiderate. Producono effetti indesiderati solo quando il rapporto tra le velocita dei processi assume valori specifici: sono i cosiddetti time-dependent errors, l'incubo della programmazione concorrente, chiamati anche heisen-bug — perche il debugging stesso influenza i bug, modificando i tempi di esecuzione e facendo sparire il problema.

Un esempio classico: Alice e Bob comprano il latte

Il professor Ricci illustra il problema della sincronizzazione con un esempio memorabile. Alice e Bob vivono insieme e sono entrambi responsabili di comprare il latte quando finisce. Senza coordinamento, puo succedere questo:

Per l'esame

L'esempio di Alice e Bob mostra come la sincronizzazione sia diversa dalla mutua esclusione. La sincronizzazione definisce relazioni temporali tra processi (es. azioni che devono avvenire prima di altre); la mutua esclusione definisce restrizioni sull'accesso a dati condivisi. La mutua esclusione richiede tipicamente forme implicite di sincronizzazione (bloccare azioni, attendere che altre completino), ma la sincronizzazione non richiede necessariamente dati condivisi o mutua esclusione.

Una possibile soluzione usa biglietti sul frigorifero. Ma anche questa fallisce se Alice si distrae e non vede il biglietto di Bob:

TempoAliceBob
5:00Arriva a casa
5:05Guarda il frigo: nessun biglietto
5:10Ops, deve andare in bagno
5:15Ancora in bagnoArriva a casa
5:20Ancora in bagnoGuarda il frigo: nessun biglietto
5:21Ancora in bagnoGuarda nel frigo: niente latte
5:22Ancora in bagnoLascia un biglietto
5:25Ancora in bagnoVa a comprare il latte
5:45Guarda nel frigo: niente latte
5:50Lascia un biglietto...

La soluzione ingenua con biglietti non basta: serve un protocollo piu robusto che garantisca l'atomicita del controllo e dell'aggiornamento dello stato condiviso.

6. Race Condition e Situazioni Critiche

Il professor Ricci introduce i concetti fondamentali degli errori nei programmi concorrenti:

Attenzione

Una race condition (o race hazard, o semplicemente race) si verifica quando due o piu processi accedono e aggiornano simultaneamente risorse condivise, e il risultato dell'aggiornamento dipende dall'ordine specifico in cui avvengono gli accessi. Le race condition portano a due tipi principali di errori: gestione scorretta di interazioni attese e presenza di interazioni impreviste.

Situazioni critiche

Le interferenze e gli errori nei programmi concorrenti possono portare a tre situazioni critiche:

Per l'esame

Distinguere bene le tre situazioni: deadlock coinvolge piu processi che si aspettano a vicenda, starvation riguarda un singolo processo perpetuamente negato nell'accesso alle risorse, livelock e simile al deadlock ma con processi che cambiano stato continuamente senza progredire. Il livelock e un caso speciale di starvation.

7. Linguaggi e Macchine Concorrenti

Per descrivere un programma concorrente servono linguaggi di programmazione concorrenti che permettano di scrivere programmi come insiemi di istruzioni da eseguire concorrentemente. Per eseguirli serve una macchina concorrente (astratta o fisica) in grado di gestire l'esecuzione di molteplici processi sequenziali, sfruttando piu processori.

Macchina concorrente

Una macchina concorrente fornisce:

I meccanismi di sincronizzazione e comunicazione si dividono in due modelli architetturali:

Approcci progettuali per linguaggi concorrenti

Il professor Ricci identifica tre approcci principali:

  1. Libreria — Linguaggio sequenziale + libreria con primitive concorrenti (es. C + PThreads).
  2. Nativo — Linguaggio progettato per la concorrenza (es. OCCAM, Ada, Erlang, Go).
  3. Ibrido — Paradigma sequenziale esteso con supporto nativo per la concorrenza (es. Java, Scala) con librerie e pattern basati su meccanismi di base (es. java.util.concurrent).

I linguaggi mainstream come C e Java forniscono supporto per la creazione e l'esecuzione di processi tramite librerie, senza estendere necessariamente il linguaggio. Per Java, pero, non e del tutto vero: la parola chiave synchronized e un'estensione del linguaggio che non avrebbe senso in un linguaggio puramente sequenziale.

Oltre i thread: uno scenario ricco

La programmazione concorrente moderna non si limita ai thread. Il panorama include:

8. Multithreading in Java: Thread e Runnable

Il professor Ricci introduce la programmazione multi-thread in Java con una precisazione importante: Java adotta un approccio ibrido. Da un lato, le classi per i thread sono librerie di base (J2SE, java.util.concurrent), indipendenti dal sistema operativo — a differenza dei PThread in C che sono specifici per sistemi POSIX. Dall'altro, il linguaggio include elementi nativi come la parola chiave synchronized e il fatto che ogni oggetto Java ha nativamente un lock associato.

Idea chiave

Java non ha voluto "bucare" il paradigma a oggetti introducendo oggetti attivi come concetto di prima classe. Se lo avesse fatto, si sarebbe aperto un mondo di ricerca (a cui il gruppo di Bologna ha contribuito) su "oggetti che inglobano il flusso di controllo". Ma non sarebbe piu stata programmazione orientata agli oggetti pura. Java ha scelto la via pragmatica: thread come oggetti normali, con il supporto del linguaggio per la sincronizzazione.

Creazione di thread in Java

Due modi principali per creare thread in Java:

Si estende la classe Thread e si sovrascrive il metodo run():

class MyThread extends Thread {
    private int id;

    MyThread(int id) {
        this.id = id;
    }

    public void run() {
        System.out.println("Thread " + id + " avviato");
        try { Thread.sleep(5000); } catch (InterruptedException e) {}
        System.out.println("Thread " + id + " done");
    }
}

// Uso:
MyThread t1 = new MyThread(1);
MyThread t2 = new MyThread(2);
t1.start();
t2.start();

Si implementa l'interfaccia Runnable e si passa l'istanza a un thread:

class MyRunnable implements Runnable {
    private String name;

    MyRunnable(String name) {
        this.name = name;
    }

    public void run() {
        System.out.println(name + " avviato");
    }
}

// Uso:
Thread t1 = new Thread(new MyRunnable("Thread-1"));
Thread t2 = new Thread(new MyRunnable("Thread-2"));
t1.start();
t2.start();

Questo approccio e preferibile perche disaccoppia il comportamento attivo dalla gerarchia di classi, permettendo alla classe di estendere altre classi se necessario.

Il metodo run() non va chiamato direttamente

Attenzione

Il metodo run() non va mai chiamato direttamente dal codice applicativo. Quando chiamiamo start(), il sistema (JVM + librerie) crea un nuovo thread del sistema operativo e invoca run() su quel thread. Se chiamiamo run() direttamente, viene eseguito sul thread corrente (quello del main) in modo sequenziale, senza creare un nuovo flusso di controllo. run() e pubblico solo per definizione del contratto dell'interfaccia — idealmente non dovrebbe essere pubblico.

Comportamento attivo e terminazione

I componenti attivi spesso non devono terminare. A differenza di una computazione tradizionale (input, processa, output, termina), i thread tipicamente hanno un comportamento ciclico, persistente. Il metodo run() non restituisce nulla (void) e non viene chiamato da noi: lo chiama la JVM sul nuovo thread.

Un esempio: ordinamento parallelo con quicksort

Il professor Ricci anticipa un esempio che sara sviluppato nella prossima lezione. L'idea e ordinare un vettore enorme usando quicksort, ma sfruttando tutti i core del processore. Nel main si genera un array di 400 milioni di elementi, si chiama Arrays.sort() (sequenziale) e si misura il tempo. La sfida e parallelizzare l'ordinamento.

9. Programmazione Asincrona e Modello Event-Loop

Il professor Ricci introduce il modulo 2.1 dedicato alla programmazione asincrona, uno stile di programmazione sempre piu importante che riguarda l'esecuzione e la gestione di computazioni/richieste/processi asincroni, astraendo dai thread. E supportato da tutti i principali framework e piattaforme: Java, .NET, iOS, Android.

Panorama della programmazione asincrona

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

Programmazione event-driven

Un programma event-driven e composto da routine (event handler) che vengono chiamate (eseguite, attivate) quando si verificano eventi: azioni dell'utente (click, tasti), output di sensori, messaggi, risposte ricevute. E "programmazione senza call stack": dopo l'esecuzione di ogni handler, lo stack e vuoto.

Eventi vs Thread: un dibattito storico

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

Ogni approccio ha pro e contro: la vera sfida e come metterli insieme.

Modello event-loop

L'architettura a event-loop e il modello di esecuzione base della programmazione asincrona:

// Pseudo-codice dell'event-loop
while (true) {
    Event ev = waitForEvent(eventQueue);
    Handler handler = selectHandler(ev);
    execute(handler);
}

Un singolo thread di controllo attende eventi ed esegue gli handler. Una coda 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 e in esecuzione, l'handler corrispondente viene eseguito solo dopo il completamento dell'handler corrente (nessuna concorrenza).

Never-blocking rule

Regola fondamentale

Gli event handler non devono mai bloccarsi (non devono contenere chiamate bloccanti) e devono sempre terminare (non devono contenere cicli infiniti). Una chiamata bloccante bloccherebbe l'intero event-loop, impedendo l'elaborazione degli eventi in coda. Una chiamata bloccante deve essere sostituita da una richiesta o computazione asincrona che generera un evento futuro (risultato o errore).

Le richieste asincrone sono servite da altri thread, indipendenti dall'event-loop, che interagiscono inserendo eventi nella coda dell'event-loop.

Pattern Reactor

Il pattern Reactor (Schmidt et al., POSA Vol. 2) e un pattern architetturale per la gestione di eventi in sistemi concorrenti. Separa la demultiplazione e la dispatch degli eventi dalla logica applicativa. Un Initiation Dispatcher attende che gli handle diventino pronti e notifica gli Event Handler registrati.

Concettualmente, ogni componente attivo puo avere il proprio event-loop privato, con la propria coda eventi e task asincroni non bloccanti. Un sistema puo essere composto da molteplici componenti attivi, ciascuno con il proprio event-loop, senza condividere memoria e interagendo tramite generazione di eventi.

10. Callback e Continuation-Passing Style

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

Callback come continuazioni

Una funzione callback viene specificata come argomento (tipicamente l'ultimo) della chiamata a un task/funzione asincrona. Il callback viene chiamato dall'event-loop quando l'evento relativo al risultato o errore viene processato. In questo modo, il callback definisce una continuazione della computazione: si parla di Continuation-Passing Style (CPS).

CPS nei linguaggi funzionali

Il Continuation-Passing Style e 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. E un formato intermedio popolare per i compilatori di linguaggi funzionali, permette l'ottimizzazione delle chiamate tail e consente una programmazione "senza stack".

Esempio di CPS in JavaScript:

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

// Versione asincrona con callback
function loadUserPic(userId, ret) {
    findUserById(userId, (user) => {
        loadPic(user.picId, ret);
    });
}

loadUserPic('john', (pic) => {
    ui.show(pic);
});

I callback sono spesso implementati come closure, che registrano non solo la funzione ma anche l'ambiente lessicale (le variabili libere) al momento della creazione.

Chi invoca la continuazione?

Due possibilita:

  1. Un thread di controllo diverso da quello che ha attivato la richiesta (es. il thread che genera l'evento) — ma questo introduce race condition.
  2. Nell'architettura event-loop, la continuazione viene eseguita dall'event-loop stesso, ossia dal thread logico che ha attivato il task asincrono (che e diverso dal thread che ha servito il task). Questo e il modello di esecuzione delle moderne app web (client e server).
Vantaggi del modello CPS con event-loop

Il problema del callback hell

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

// Pyramid of doom
step1(function(result1) {
    step2(function(result2) {
        step3(function(result3) {
            // ... e cosi via
        });
    });
});

"I love async, but I can't code like this" — un commento tipico degli sviluppatori Node.js.

11. Promise: Stati, Chaining e Composizione

Il problema del callback hell puo essere parzialmente risolto con il meccanismo delle Promise. Originariamente proposte nel 1976 da Daniel Friedman e D. Wise, le Promise sono oggetti proxy che rappresentano un risultato ancora da calcolare (simili ai Future). Incapsulano azioni asincrone e si comportano come valori restituiti — solo che il valore potrebbe non essere ancora disponibile.

Ciclo di vita di una Promise

Una volta che una Promise e settled (risolta o rigettata), il suo stato e valore non cambiano piu: sono immutabili.

Promise in JavaScript

// 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"
});

La proprieta chiave delle Promise e che il metodo then restituisce a sua volta una Promise, permettendo il chaining. Questo appiattisce la piramide delle callback:

// Chaining: appiattisce la piramide
findUserById('john')
    .then((user) => {
        return findPic(user.picId);
    })
    .then((pic) => {
        ui.show(pic);
    });
Propagazione dei valori

Il valore di ogni Promise in una catena e 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.

Pattern di esecuzione

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

Error handling con Promise

Un miglioramento significativo rispetto alle callback: qualsiasi handler che restituisce un valore e non lancia eccezioni passa il successo alla Promise successiva; qualsiasi handler che lancia un'eccezione passa un reject alla Promise successiva. Lo stato non gestito viene propagato lungo la catena finche non trova un handler di errore.

Limiti delle Promise

Problemi noti

12. Async/Await: Sintassi Sincrona per Codice Asincrono

L'estensione linguistica async / await e la soluzione piu recente e promettente nella programmazione asincrona. Introdotta in ES2017 (8a edizione dello standard ECMAScript), implementata in .NET TAP, Python, DART e molti altri linguaggi. L'obiettivo e fornire uno stile di programmazione sincrono mantenendo un nucleo asincrono.

Idea fondamentale

// Stile Promise
let p = asyncFunc();
p.then(onComplete, onError);

// Stile async/await: riproduce la sintassi sincrona
let res = await asyncFunc();
// usa res

L'operatore await si applica a una Promise e sospende l'esecuzione della funzione finche la Promise non e soddisfatta — senza bloccare il flusso di controllo (il controllo viene ceduto, salvando lo stato corrente della computazione).

Solo dentro funzioni async

await puo essere usato solo nel corpo di funzioni marcate come async:

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

Le funzioni async restituiscono Promise

Una funzione async e 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

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

Problemi aperti

Criticita

13. Coroutine, Fiber e Virtual Threads

Il professor Ricci conclude con una panoramica sulle basi concettuali che abilitano async/await: le coroutine.

Coroutine

Le coroutine sono una generalizzazione del concetto di subroutine che permette all'esecuzione di essere sospesa e ripresa (Conway, 1963). Sono il mattoncino fondamentale per implementare task cooperativi, eccezioni, event-loop, iteratori, liste infinite e pipe. Non sono legate agli eventi o alla programmazione event-driven — storicamente 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).

Coroutine come routine generalizzate

Quando si chiama una coroutine, essa puo 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 piu istanze della stessa coroutine simultaneamente.

Esempio classico: sistema produttore-consumatore cooperativo a singolo flusso:

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

Modello di concorrenza basato su coroutine

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

Kotlin e un esempio moderno che integra le coroutine nel linguaggio con concetti come suspending computations, coroutine dispatchers e coroutine builders (async, launch, runBlocking).

import kotlinx.coroutines.*

suspend fun greetAfter(name: String, delayMillis: Long) {
    delay(delayMillis)
    println("Hello, $name")
}

fun main() = runBlocking {
    val result = async {
        delay(1000)
        100
    }
    println("${result.await()}")
}

Fiber (fibre)

Una fiber (fibra) e 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. Sono simili alle coroutine ma a un diverso livello di astrazione: le coroutine sono un costrutto linguistico (forma di controllo del flusso), le fibre sono un costrutto di sistema (viste come thread che non girano in parallelo). Implementate da UNIX/PThreads, Windows/.NET.

Virtual Threads in Java

A partire da JDK 19, Java introduce i Virtual Threads. Il professor Ricci cita l'articolo di Brian Goetz (Goe22): i virtual threads forniscono una migliore modularita e incapsulamento per organizzare processi e flussi di controllo logici (rispetto alla programmazione asincrona). Tuttavia, come i thread fisici, non forniscono un trattamento di prima classe 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 e ancora aperto. I virtual threads offrono un modello piu semplice (un thread per task, senza dover riscrivere il codice in stile asincrono), ma non risolvono il problema della gestione degli eventi e della reattivita, che richiede comunque astrazioni aggiuntive a livello applicativo.

Verifica le tue conoscenze

Qual e la differenza fondamentale tra concorrenza e parallelismo?

La concorrenza e un concetto di progettazione del software: riguarda la composizione di computazioni eseguite indipendentemente, e un modo di strutturare il programma (livello logico/astratto). Il parallelismo riguarda l'esecuzione fisica su processori separati, focalizzato sulla performance (livello fisico). Come dice Rob Pike: "La concorrenza non e parallelismo, lo abilita."

Cosa afferma la legge di Amdahl e quali sono le sue implicazioni pratiche?

La legge di Amdahl afferma che lo speedup massimo ottenibile parallelizzando un programma e limitato dalla parte sequenziale: S = 1 / ((1-P) + P/N), dove P e la frazione parallelizzabile e N il numero di processori. Implicazione pratica: aggiungere processori aiuta solo sulla parte parallelizzabile; la parte sequenziale diventa il collo di bottiglia. Anche con infiniti processori, lo speedup massimo e 1/(1-P).

Quali sono le differenze tra deadlock, starvation e livelock?

Deadlock: due o piu processi sono in attesa che l'altro rilasci una risorsa, nessuno progredisce. Coinvolge piu processi. Starvation: un processo viene perpetuamente negato nell'accesso a risorse necessarie, non riesce mai a completare. Riguarda un singolo processo. Livelock: simile al deadlock, ma i processi cambiano attivamente stato in risposta l'uno all'altro, senza pero progredire. E un caso speciale di starvation.

Perche in Java non si deve mai chiamare direttamente il metodo run() di un thread?

Il metodo run() contiene il comportamento attivo del thread, ma non va chiamato direttamente. Quando si chiama start(), la JVM crea un nuovo thread del sistema operativo e invoca run() su quel thread. Se si chiama run() direttamente, viene eseguito sul thread corrente (quello del main) in modo sequenziale, senza creare un nuovo flusso di controllo. run() e pubblico per contratto dell'interfaccia, ma concettualmente non dovrebbe esserlo.

Quali sono i tre stati di una Promise e cosa significa "settled"?

Una Promise ha tre stati: pending (stato iniziale, appena creata), resolved o fulfilled (il task asincrono e terminato correttamente), rejected (il task asincrono e terminato con errore). Una Promise si dice settled quando non e piu in pending, cioe e resolved o rejected. Una volta settled, stato e valore sono immutabili.

Cosa risolve e cosa non risolve async/await rispetto alle Promise?

Async/await risolve: il problema dell'eagerness, il parametro di then, l'uso con loop/iterazioni. Semplifica la programmazione asincrona permettendo uno stile sincrono. Non risolve invece: la necessita di mescolare async/await con l'API Promise per composizioni complesse (es. Promise.all), il problema di modularita/incapsulamento (chiamare una funzione async senza await produce comportamento diverso), il design clash tra codice sincrono e asincrono, e i blocchi non piu atomici (possibili race condition).

Quali sono i tre approcci progettuali per i linguaggi di programmazione concorrenti?

1) Linguaggio sequenziale + libreria: es. C + PThreads. 2) Linguaggio nativo per concorrenza: es. OCCAM, Ada, Erlang, Go. 3) Approccio ibrido: paradigma sequenziale esteso con supporto nativo alla concorrenza + librerie, es. Java con synchronized e java.util.concurrent.

Cosa sono i Virtual Threads in Java e quali sono i loro limiti?

Introdotti da JDK 19, i Virtual Threads sono thread leggeri gestiti dalla JVM che non richiedono context switching da parte dell'OS. Permettono un modello di programmazione piu semplice (un thread per task, senza dover riscrivere il codice in stile asincrono). Limite principale: come i thread fisici, non forniscono un trattamento di prima classe per eventi e computazioni reattive, che vanno gestiti a livello applicativo.

Quali sono le quattro categorie della tassonomia di Flynn?

SISD (Single Instruction, Single Data): processori single-core, modello Von Neumann. SIMD (Single Instruction, Multiple Data): processori vettoriali, GPU. MISD (Multiple Instruction, Single Data): nessun sistema noto di uso comune. MIMD (Multiple Instruction, Multiple Data): la maggior parte dei sistemi moderni, si divide in memoria condivisa (SMP, NUMA) e memoria distribuita (MPP, Cluster, Grid).

Perche l'esempio di Alice e Bob con i biglietti sul frigo non risolve il problema del latte?

La soluzione con i biglietti e basata sull'idea di controllare se c'e un biglietto prima di agire. Ma cade in un classico problema di race condition: Alice controlla il frigo, non trova biglietti, ma prima di lasciare il proprio biglietto si distrae (va in bagno). Nel frattempo Bob verifica la stessa condizione e lascia un biglietto. Quando Alice torna, non rivede la scena e lascia un altro biglietto. Il problema e che le operazioni di "controllo" e "scrittura" non sono atomiche. Serve un meccanismo di mutua esclusione.