Il Prof. Alessandro Ricci apre il corso di Programmazione Concorrente e Distribuita (PCD) presso l'Universita' di Bologna, sede di Cesena. Il corso e' integrato con un'altra disciplina: le due parti sono indipendenti ma complementari. Il voto finale sara' la media dei voti ottenuti nelle due parti.
Il corso si inserisce nel primo anno della laurea magistrale in Ingegneria e Scienze Informatiche. L'obiettivo e' consolidare competenze trasversali fondamentali, a prescindere dalla specializzazione successiva (data science, AI, system development).
L'esame si compone di due macro-parti:
| Parte | Descrizione |
|---|---|
| Pratica (Assignments) | Quattro piccoli progetti distribuiti durante l'anno, senza scadenza. Servono per applicare subito quanto imparato e costituiscono il portfolio da discutere all'orale. |
| Orale | Colloquio a partire dal portfolio degli assignments. Si discutono i progetti, le scelte progettuali e i concetti teorici sottostanti. |
Gli assignments non hanno deadline: lo studente organizza autonomamente i tempi. Non sono esami parziali, ma strumenti per consolidare l'apprendimento. Il professore sottolinea che questa flessibilita' e' una novita' rispetto agli anni passati.
Il professor Ricci specifica che e' consentito l'uso dell'AI negli assignment, a patto che venga usata in modo opportuno: l'importante e' che lo studente sappia cosa ha fatto e sappia discuterne al colloquio. L'AI va usata come supporto, non come sostituto della comprensione.
Il colloquio orale non e' un esame tradizionale: si parte dalla discussione degli assignment (cosa avete fatto, come, perche') e da li' si sviluppa una conversazione sugli argomenti del corso. E' quindi essenziale aver svolto gli assignment in modo consapevole.
Oltre ai quattro assignment standard, quest'anno viene introdotta una novita' pensata per agevolare gli studenti lavoratori: la possibilita' di concordare un colloquio personalizzato in cui gli assignment vengono sostituiti o ritagliati su un progetto reale legato all'attivita' lavorativa dello studente.
Se sei uno studente lavoratore o stai seguendo un progetto significativo in altre materie, puoi contattare il docente per concordare un esame su misura: il contenuto della prova pratica viene adattato al progetto o all'attivita' lavorativa. L'obiettivo e' evitare la sovrapposizione di impegni, mantenendo la verifica delle competenze.
Il docente ricorda che le lezioni vengono regolarmente registrate e messe a disposizione proprio per facilitare chi deve organizzarsi in modo flessibile, non solo gli studenti lavoratori.
Uno sguardo dalla chiusura della parte introduttiva del corso aiuta a orientare lo studio fin dall'inizio: da un certo punto in poi il focus si sposta verso la parte distribuita, usando gli strumenti gia' visti per leggere protocolli, concorrenza e coordinazione.
Nel ripasso finale tornano utili anche clock logici e vector clock: non come teoria isolata, ma come strumenti per capire l'ordine degli eventi nei sistemi distribuiti.
| Asse | Domanda da saper rispondere |
|---|---|
| Actor model | Perche' elimina shared mutable state e riduce le race? |
| Asincronia | Come si coordina una risposta quando il mittente non aspetta? |
| Parte distribuita | Come ragioniamo su ordine, causalita' e protocolli? |
Cosi' il docente scandiva il calendario delle ultime lezioni:
Oggi: ripasso degli actors e orientamento finale. L'obiettivo e' consolidare il modello mentale, non aggiungere complessita' nuova.
Venerdi: modulo 4.1 e passaggio alla parte distribuita. Qui i concetti vanno letti come estensione naturale di cio' che gia' conoscete.
Lunedi: anticipazione del quarto assignment, cosi' avete tempo di leggere la traccia con calma e prepararvi prima della consegna.
Il laboratorio del corso e' 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.
La concorrenza e' una proprieta' dei sistemi in cui piu' processi computazionali sono in esecuzione contemporaneamente e potenzialmente interagiscono tra loro. E' un concetto che attraversa molti domini: sistemi operativi, programmi multi-thread e multi-processo, sistemi distribuiti, sistemi di controllo, sistemi real-time.
Un programma concorrente specifica due o piu' programmi sequenziali che possono essere eseguiti concorrentemente come processi paralleli. L'esecuzione di un programma concorrente si chiama computazione concorrente o elaborazione concorrente.
La programmazione concorrente e' l'arte (o disciplina) di costruire programmi in cui molteplici attivita' computazionali si sovrappongono nel tempo e tipicamente interagiscono in qualche modo. 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.
Ogni programma sequenziale in esecuzione viene chiamato processo: un singolo thread di controllo, una sequenza di istruzioni che opera come un gruppo. Il termine e' astratto e non va confuso con il processo del sistema operativo.
| Proprieta' | Significato |
|---|---|
| Indipendenza dalla velocita' | L'esecuzione dei processi e' completamente asincrona: non si possono fare assunzioni sulla loro velocita' relativa. |
| Non-determinismo | L'ordine di interleaving delle istruzioni non e' deterministico, puo' variare a ogni esecuzione. |
Il professor Ricci fa l'esempio dei sistemi operativi: in un sistema moderno, decine o centinaia di processi convivono. Il sistema operativo e' un esempio classico di sistema concorrente, dove la concorrenza emerge dalla gestione di eventi asincroni (interrupt), scheduler, e interazioni tra processi.
La programmazione concorrente si declina in diversi paradigmi:
| Paradigma | Stato condiviso | Meccanismi |
|---|---|---|
| Multi-threaded | Si, memoria condivisa | Semafori, monitor, synchronized |
| Message-based | No | Scambio di messaggi asincrono/sincrono |
| Event-driven | Variabile | Event-loop, handler, callback |
| Asincrono | No (tipicamente) | Future, Promise, async/await |
| Reactive | Variabile | Data flow, propagation of change |
E' fondamentale distinguere tre concetti spesso confusi:
Livello logico/astratto. Si concentra sull'organizzazione del programma: strutturare il software come insieme di attivita' che si sovrappongono, componendo computazioni eseguite indipendentemente. Non richiede necessariamente piu' processori fisici.
Livello fisico. L'esecuzione dei programmi si sovrappone nel tempo perche' eseguita su processori fisici separati. L'attenzione e' sulle performance: speedup e uso efficiente delle risorse hardware.
Rete. I processori sono distribuiti su una rete, senza memoria condivisa. La comunicazione avviene esclusivamente tramite scambio di messaggi.
Il professor Ricci cita Rob Pike (co-creatore di Go): la concorrenza e' un modo di strutturare il software, di comporre computazioni indipendenti. Il parallelismo e' un aspetto algoritmico, legato alle performance. La concorrenza abilita il parallelismo, ma non e' parallelismo.
"Concurrency is the composition of independently executing computations. It is a way of structuring software, to write clean code that interacts well with the real world. It is not parallelism; it enables parallelism." — Rob Pike. In italiano: "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."
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.
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' presentati nella sezione 2, 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.
Herb Sutter ha coniato l'espressione "The Free Lunch is Over": per decenni i programmatori hanno beneficiato dell'aumento automatico delle prestazioni grazie all'incremento della frequenza dei processori, senza dover riscrivere le applicazioni. Questa era e' finita. Oggi le prestazioni si migliorano aggiungendo core, non aumentando la frequenza.
I processori moderni integrano multi-core sullo stesso chip, condividendo RAM e talvolta livelli di cache. Esempi iconici: 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.
Le architetture recenti sono ibride, combinando core di dimensioni diverse:
| Tipo | Caratteristiche |
|---|---|
| Performance-cores (P-core) | Design tradizionale, frequenze elevate (es. 5.8 GHz), massima potenza di calcolo. |
| Efficient-cores (E-core) | Core piu' lenti ma fisicamente piu' piccoli, consumano molta meno energia. Ideali per task in background. |
Esempio: Intel Core i9-14th Gen — 24 core, fino a 32 thread (8 P-core a 5.8 GHz + 16 E-core per task in background). Anche AMD con l'architettura Zen 5 adotta un approccio simile, con chiplet design che collega piccole unita' (Core Complex Dies, CCD) tramite un'interconnessione (fabric) ad alta velocita', scalando fino a 16 core/32 thread su desktop e oltre 128 core su server.
I chip moderni integrano processori specializzati: GPU, NPU (Neural Processing Unit), ISP (Image Signal Processor), FPGA. Esempio: Apple Silicon M5 — System-on-Chip con CPU (fino a 10 core, P-core + E-core), GPU, NPU per machine learning, e vari coprocessori. Ancora piu' estremi i sistemi dedicati all'AI, come il Cerebras CS-3.
I supercomputer (es. Fugaku del RIKEN Center, realizzato da Fujitsu, con oltre 7,6 milioni di core) hanno milioni di core connessi da reti ad-hoc. I cluster usano componenti commodity (computer standard) connessi da reti standard (Gigabit Ethernet, InfiniBand). Il cloud computing (AWS EC2, Azure, Google App Engine) offre risorse come servizio su rete pubblica.
I supercomputer nella top 500 usano prevalentemente Linux. L'Italia e' presente nella classifica. La lista viene aggiornata periodicamente (novembre 2025 e' l'ultima citata nel corso).
L'evoluzione hardware appena vista ha reso la programmazione concorrente non piu' un'opzione ma una necessita'. Per orientarsi tra le architetture serve una classificazione.
Herb Sutter nel 2005 scrisse il famoso articolo «The Free Lunch Is Over»: la crescita della frequenza dei processori si era arrestata per limiti fisici (dissipazione termica), e l'incremento di potenza sarebbe venuto solo da piu' core. La programmazione sequenziale non avrebbe piu' beneficiato di speedup gratuiti.
La tassonomia di Flynn classifica tutti i sistemi di calcolo in base al numero di flussi di istruzioni e flussi di dati:
| Classe | Descrizione | Esempio |
|---|---|---|
| SISD (Single Instruction, Single Data) | Un flusso di istruzioni, un flusso di dati. | Modello di Von Neumann, processori single-core. |
| SIMD (Single Instruction, Multiple Data) | Un flusso di istruzioni trasmesso a piu' processori, ognuno con i propri dati. Parallelismo fine. | Processori vettoriali, GPU. |
| MISD (Multiple Instruction, Single Data) | Piu' flussi di istruzioni su unico flusso di dati. | Nessun sistema noto di uso comune implementa questo modello. |
| MIMD (Multiple Instruction, Multiple Data) | Ogni processore ha il proprio flusso di istruzioni e i propri dati. | La stragrande maggioranza dei sistemi moderni: multi-core, cluster. |
La categoria MIMD si suddivide ulteriormente in base all'organizzazione della memoria:
Tutti i processi condividono un unico spazio di indirizzi. Comunicano leggendo e scrivendo variabili condivise. Due sottoclassi:
Ogni processo ha il proprio spazio di indirizzi. Comunicano tramite scambio di messaggi (inviare e ricevere messaggi). Sottoclassi:
In sintesi:
| Modello | Memoria | Comunicazione |
|---|---|---|
| Memoria condivisa | Singolo spazio di indirizzi | Lettura/scrittura di variabili condivise |
| Memoria distribuita | Ogni processo ha il proprio spazio | Message passing (send/receive) |
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.
Uno degli aspetti fondamentali quando si scrivono programmi concorrenti e' capire se l'uso di piu' thread porta effettivamente a un miglioramento delle prestazioni. Il tempo assoluto di esecuzione e' significativo, ma per valutare l'efficacia della parallelizzazione si usano due metriche principali: lo speedup e l'efficienza.
Lo speedup misura quanto piu' veloce e' l'esecuzione parallela rispetto a quella sequenziale. L'efficienza misura quanto bene vengono sfruttati i processori disponibili.
Lo speedup misura il miglioramento delle prestazioni quando si usa un algoritmo parallelo rispetto a quello sequenziale: e' il rapporto fra il tempo di esecuzione sequenziale T1 e il tempo di esecuzione parallelo TN con N processori.
S = T1 / TN
T1 = tempo di esecuzione con 1 processore
TN = tempo di esecuzione con N processori
N = numero di processori
Uno speedup ideale e' lineare: S = N. In pratica e' quasi sempre inferiore a causa della parte sequenziale del programma e dei costi di coordinamento.
La legge di Amdahl e' il modello matematico fondamentale per capire il limite del parallelismo: stabilisce un limite teorico allo speedup ottenibile parallelizzando un programma. Se P e' la proporzione del programma che puo' essere parallelizzata, la parte rimanente (1 − P) e' intrinsecamente sequenziale:
S = 1 / ((1 - P) + P/N)
P = proporzione del programma parallelizzabile
1 - P = parte non parallelizzabile (sequenziale)
Al crescere di N, lo speedup tende a 1 / (1 − P): e' la parte sequenziale a dominare il limite massimo. Se il 10% del programma e' intrinsecamente sequenziale, lo speedup massimo non puo' superare 10, indipendentemente dal numero di processori.
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→∞"]
Il simulatore seguente aggiunge al calcolo anche l'efficienza e la curva dello speedup al variare di N:
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.
La parte sequenziale (1-P) ha un impatto drammatico sulle performance. Spesso la sequenzializzazione e' necessaria per la correttezza (es. lock per evitare corse critiche, sincronizzazione). Una gestione inefficiente dei lock puo' vanificare i benefici di architetture multi-core. La legge di Amdahl ci ricorda che migliorare solo la parte parallela non basta: bisogna ridurre anche la serializzazione.
L'efficienza e' una misura normalizzata dello speedup che indica quanto efficacemente ogni processore viene utilizzato:
E = S / N
S = speedup, N = numero di processori
L'efficienza ideale e' 1 (tutti i processori usati a piena capacita'), ma in pratica e' quasi sempre inferiore a causa della parte sequenziale, del costo della comunicazione e della sincronizzazione tra processori. Valori bassi indicano overhead di parallelizzazione, contention o parti sequenziali. In alcuni casi particolari si puo' osservare uno speedup super-lineare (S > N), ad esempio per effetto della cache.
Oltre alla legge di Amdahl, un altro fattore limitante e' la memoria condivisa e il bus: solo un'operazione di memoria puo' avvenire alla volta sul bus. Da qui l'importanza della cache e dei protocolli di coerenza della cache, sempre piu' complessi, sofisticati e intelligenti.
Quando si misurano le performance di un programma concorrente, bisogna tenere conto di:
Il docente mostra un esempio pratico chiamato IO-Bound Test per illustrare come il comportamento dei thread cambi radicalmente a seconda del tipo di carico di lavoro.
Un worker CPU-bound esegue solo computazione pura: in questo esempio ordina ripetutamente un array per simulare lavoro intensivo sulla CPU. Lanciando piu' thread CPU-bound su una macchina con, diciamo, 10 core:
Il professore mostra il monitor con Mission Control (o JConsole): si osserva che con 1 thread la CPU usage resta bassa, mentre con piu' thread sale vicino al 100%, segno che tutti i core sono saturati.
Un worker IO-bound simula operazioni di input/output (es. sleep, attesa di rete, lettura file). In questo caso i thread passano gran parte del tempo bloccati in attesa. La CPU rimane relativamente scarica anche con molti thread (es. 124 thread mostrano solo 6-9% di CPU usage).
Nei workload IO-bound, aumentare il numero di thread oltre il numero di core e' utile: mentre un thread e' bloccato in I/O, un altro thread puo' usare la CPU. Nei workload CPU-bound, superare il numero di core peggiora le prestazioni per il context switch.
Il worker nel test e' progettato con un ciclo while che alterna casualmente fasi di computazione CPU (ordinamento array) e fasi di I/O (sleep). Questo evita la sincronizzazione perfetta in cui tutti i thread farebbero I/O contemporaneamente, simulando uno scenario piu' realistico dove alcuni thread computano mentre altri attendono.
// Pseudo-struttura del worker
public void run() {
while (true) {
if (casuale()) {
// Fase CPU-bound: ordina array ripetutamente
ordinaArray();
} else {
// Fase IO-bound: attesa (sleep)
Thread.sleep(5); // 5 ms di attesa
}
}
}
Usando JConsole si puo' monitorare l'utilizzo della CPU in tempo reale. Con workload IO-bound si nota che la CPU rimane poco utilizzata anche con molti thread, mentre con workload CPU-bound si avvicina al 100%. Strumenti piu' sofisticati come JProfiler (non piu' open source) o VisualVM permettono di analizzare nel dettaglio il comportamento dei singoli thread, individuando lock contention, thread bloccati e bottleneck di performance.
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 (fisici o virtuali).
Una macchina concorrente fornisce il supporto per eseguire programmi concorrenti e realizzare computazioni concorrenti, mettendo a disposizione tanti processori virtuali quanti sono i processi della computazione concorrente.
La macchina concorrente fornisce tanti processori virtuali quanti sono i processi del programma concorrente. Poi, un livello sottostante (sistema operativo, virtual machine, hardware) mappa questi processori virtuali sui processori fisici disponibili.
La macchina concorrente fornisce tre categorie di meccanismi di base:
Un esempio concreto: Java Virtual Threads. Un programma puo' creare decine di migliaia di virtual thread come entita' logiche, che vengono poi eseguiti da un insieme di thread fisici pari (tipicamente) al numero di processori disponibili. E' la macchina concorrente (la JVM) che si occupa di questo mapping.
Analogamente, nel modello Attori (che useremo in laboratorio), si possono creare milioni di attori — entita' logiche che comunicano tramite messaggi — e la macchina concorrente li mappa efficientemente sulle risorse hardware parallele.
Il professor Ricci identifica tre approcci principali:
| Approccio | Esempi | Descrizione |
|---|---|---|
| Linguaggio sequenziale + libreria | C + PThreads | Si aggiungono primitive concorrenti tramite librerie esterne. |
| Linguaggio nativo per concorrenza | OCCAM, ADA, Erlang, Go | Il linguaggio e' progettato fin dall'inizio per la concorrenza. |
| Approccio ibrido | Java, Scala | Paradigma sequenziale esteso con supporto nativo alla concorrenza + 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.
Il panorama della programmazione concorrente moderna va ben oltre i thread tradizionali:
Le domande provengono dalle due lezioni introduttive complete: alcune anticipano temi (thread Java, event-driven, Promise, async/await, coroutine) trattati in dettaglio nei capitoli successivi.
La programmazione concorrente e' un modo di strutturare il software come insieme di attivita' che si sovrappongono, al livello logico/astratto. Il parallelismo riguarda l'esecuzione fisica su processori separati, con focus sulle performance. La concorrenza abilita il parallelismo ma non 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."
Herb Sutter ha coniato questa espressione per indicare la fine dell'era in cui le prestazioni dei programmi aumentavano automaticamente grazie all'incremento della frequenza dei processori. Oggi il miglioramento delle prestazioni passa attraverso l'aumento del numero di core, richiedendo programmazione concorrente per sfruttarli.
Lo speedup massimo ottenibile parallelizzando un programma e' limitato dalla porzione non parallelizzabile: S = 1 / ((1-P) + P/N). Anche con P=0.95 (95% parallelizzabile), lo speedup massimo e' 20x. Se il 50% e' sequenziale, lo speedup non supera 2x qualsiasi sia il numero di core.
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).
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).
MPP (Massively Parallel Processors): processori e rete strettamente accoppiati e specializzati per HPC. Cluster: computer commodity connessi da rete standard (es. Beowulf). Grid: risorse eterogenee distribuite su LAN/WAN senza un punto di amministrazione comune.
La sincronizzazione definisce relazioni temporali tra processi (precedenza, simultaneita'), mentre la mutua esclusione definisce restrizioni sull'accesso a dati condivisi. La mutua esclusione richiede forme implicite di sincronizzazione, ma la sincronizzazione non richiede necessariamente dati condivisi.
Alice e Bob controllano entrambi il frigo. Se non c'e' latte e non c'e' un biglietto, lasciano un biglietto, comprano il latte, lo mettono in frigo e rimuovono il biglietto. Il problema: a causa dell'interleaving delle operazioni, entrambi possono controllare il frigo simultaneamente, vedere nessun biglietto, lasciare ciascuno un biglietto e comprare entrambi il latte. E' un classico esempio di race condition dovuta a mancanza di atomicita' nell'operazione di controllo + scrittura.
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.
Deadlock: due o piu' processi attendono che l'altro finisca e nessuno procede. Starvation: un singolo processo e' perpetuamente negato nell'accesso alle risorse. Livelock: come il deadlock ma gli stati dei processi cambiano continuamente senza che nessuno progredisca (un caso speciale di starvation).
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.
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.
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.
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.
La regola never-blocking: gli event handler non devono mai bloccare (ne' contenere chiamate bloccanti o loop infiniti). Una chiamata bloccante bloccherebbe l'intero event loop, impedendo il processamento degli eventi in coda. Le operazioni bloccanti devono essere sostituite da richieste asincrone.
Le Promise sono oggetti proxy che rappresentano un risultato futuro di un'operazione asincrona. Risolvono il problema del callback hell appiattendo l'annidamento tramite then chaining e migliorando la gestione degli errori. Limiti: eagerness (esecuzione immediata), impossibilita' di passare parametri in then, nessun supporto a cicli, non cancellabili.
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.
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).
Le coroutine sono una generalizzazione delle subroutine che permette di sospendere e riprendere l'esecuzione. Il meccanismo async/await e' implementato tipicamente usando coroutine a basso livello: await sospende la funzione (yielding il controllo) e la coroutine viene ripresa asincronamente quando l'evento corrispondente e' pronto nella coda dell'event loop.