Il capitolo in un paragrafo. Un programma concorrente è un insieme di programmi sequenziali, i processi, le cui esecuzioni si sovrappongono nel tempo e interagiscono. Concorrenza non vuol dire parallelismo: la prima è un modo di organizzare il software e si ha anche con una sola CPU, il secondo è l'esecuzione simultanea su processori fisici distinti. Da quando la frequenza dei processori ha smesso di crescere, le prestazioni arrivano solo da più core, e la legge di Amdahl dice quanto se ne può guadagnare. Il corso però usa la concorrenza soprattutto come strumento di progettazione, per costruire sistemi fatti di componenti attivi che interagiscono.
Il corso (6 CFU, Prof. Alessandro Ricci con Nicolas Farabegoli) è un modulo del corso integrato di Sviluppo di sistemi software, e il voto finale è la media dei due moduli. Gli esempi sono in Java, ma negli assignment va bene qualsiasi linguaggio che offra le stesse astrazioni. Le parti sono quattro:
L'esame ha due parti:
Conta la qualità del progetto: un programma che funziona non basta, serve un design modulare e pulito. Gli studenti lavoratori possono concordare una prova pratica sul proprio progetto di lavoro.
Le definizioni della letteratura (Roscoe; Cleaveland e Smolka) concordano su due punti: in un sistema concorrente ci sono più attività la cui esecuzione si sovrappone nel tempo (una inizia prima che l'altra sia finita), e queste attività hanno dipendenze, quindi interagiscono.
Un programma concorrente è un insieme finito di programmi sequenziali che possono essere eseguiti in parallelo, cioè sovrapposti nel tempo. L'esecuzione di un programma sequenziale si chiama processo, e l'esecuzione dell'intero programma concorrente computazione concorrente. Qui «processo» è un concetto astratto: un flusso di controllo sequenziale, un'unità di lavoro. Non coincide con il processo del sistema operativo, e un thread Java è a tutti gli effetti un processo in questo senso. Due caratteristiche accompagnano la definizione:
print a1; print a2 e Q esegue print b1; print b2, l'output può essere uno qualsiasi dei 6 intrecci che rispettano l'ordine interno di ciascun processo (a1 prima di a2, b1 prima di b2). Il capitolo 2 costruisce su questa idea il modello a interleaving.La concorrenza non richiede più processori: con una sola CPU il sistema operativo esegue un pezzo di un processo, lo sospende e passa a un altro. Le esecuzioni si sovrappongono nel tempo anche se in ogni istante ne avanza una sola.
Ogni programma concorrente non banale è fatto di processi che devono interagire. Il vocabolario dell'interazione (cooperazione, competizione, interferenza, race condition) è nel capitolo 2, le situazioni critiche (deadlock, starvation, livelock) nei capitoli 3 e 6.
| Livello | Che cosa conta | |
|---|---|---|
| Programmazione concorrente | logico: attività che si sovrappongono nel tempo e interagiscono, anche su un solo processore | l'organizzazione del programma |
| Programmazione parallela | fisico: le esecuzioni si sovrappongono perché girano su processori fisici distinti | le prestazioni |
| Programmazione distribuita | i processori sono sparsi su una rete e non condividono la memoria | comunicazione solo con messaggi |
Ogni programma parallelo è concorrente, ma non viceversa. Un sistema distribuito è concorrente per definizione e aggiunge due difficoltà: niente memoria condivisa, quindi ogni interazione passa per messaggi, e niente clock condiviso, quindi non si può stabilire con certezza quale di due eventi sia avvenuto prima. Servono allora gli orologi logici del capitolo 17.
La distinzione è il punto del talk di Rob Pike, uno dei progettisti di Go, Go Concurrency Patterns: il mondo è fatto di agenti indipendenti che interagiscono, e la programmazione sequenziale non lo cattura.
«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.»
La concorrenza è un modello per costruire software facile da capire e da ragionare; il parallelismo è una questione algoritmica e di prestazioni. Un programma ben strutturato in modo concorrente è pronto a sfruttare il parallelismo quando l'hardware c'è.
Le motivazioni sono due. La prima sono le prestazioni:
La seconda, centrale in questo corso, è la progettazione. La concorrenza è un nuovo livello di astrazione: cambia il modo di risolvere i problemi, anche per algoritmi e strutture dati di base, e il modo di scomporre, modularizzare e incapsulare i sistemi. Tocca tutto il ciclo di sviluppo: modellazione, progetto, implementazione, verifica e test. Gli oggetti dell'OOP non bastano. Un oggetto incapsula stato e comportamento, ma non il flusso di controllo: i suoi metodi girano nel thread di chi li chiama, e leggendo la classe non si sa quali thread la eseguiranno. Il corso introduce invece componenti attivi, che incapsulano un proprio flusso di controllo, accanto a componenti passivi. È la disciplina del capitolo 8.
Ogni paradigma concorrente offre astrazioni diverse per questo scopo:
| Paradigma | Idea | Nel corso |
|---|---|---|
| Multi-thread | stato condiviso, sincronizzazione con semafori e monitor | capitoli 4–7, 10 |
| A messaggi | niente stato condiviso, interazione solo con messaggi, sincroni o asincroni | capitoli 14–16 |
| A eventi | il flusso del programma è determinato da eventi: azioni dell'utente, sensori, messaggi | capitolo 12 |
| Asincrona | azioni e richieste asincrone, dogma del never blocking, future e callback | capitolo 12 |
| Reattiva | il programma è organizzato attorno a flussi di dati e alla propagazione dei cambiamenti | capitolo 13 |
Fino ai primi anni 2000 ogni generazione di processori aveva una frequenza più alta, e lo stesso programma sequenziale diventava più veloce gratis. Attorno al 2005 la frequenza si è fermata a pochi GHz per limiti fisici, e i transistor in più sono diventati core in più. Herb Sutter lo riassunse in «The free lunch is over» (2005): un programma sequenziale usa un solo core, e per sfruttare gli altri va scritto concorrente. Nel 2011 aggiunse «Welcome to the hardware jungle»: oggi i core non sono nemmeno tutti uguali.
| Architettura | Caratteristiche | Esempi dalle slide |
|---|---|---|
| Multi-core | più core sullo stesso chip, che condividono la RAM e a volte livelli di cache | Intel Core i7 (2009); AMD Threadripper 3990X (2020), 64 core |
| Multi-core ibride | P-core veloci e affamati di energia, E-core più piccoli ed efficienti per i compiti in background | Intel Core i9 di 14ª gen.: 8 P-core e 16 E-core, 32 thread; AMD Zen 5 a chiplet (core Zen 5 e Zen 5c) |
| Eterogenee | un processore affiancato da unità specializzate (attached processors): GPU, usate anche per calcolo generico (GPGPU, CUDA), FPGA, NPU per le reti neurali; al limite, chip interamente dedicati all'AI | Apple M5, system-on-chip con CPU, GPU, NPU e coprocessori; Cerebras CS-3 |
| Supercomputer | moltissimi processori, di solito senza memoria condivisa, collegati da reti costruite apposta | Fugaku (RIKEN, Fujitsu): 7,6 milioni di core |
| Cluster e grid | nodi e reti commerciali (Gigabit Ethernet, InfiniBand); comunicano a messaggi | System X (Virginia Tech) |
| Cloud | calcolo come servizio in rete (SaaS, PaaS, IaaS), pubblico o privato | Amazon EC2, Azure, Google App Engine |
La differenza tra multi-core e core eterogenei: nel primo caso i core sono processori general purpose dello stesso tipo, al più con velocità diverse come P ed E. Nel secondo si aggiungono unità di natura diversa: una GPU, per esempio, applica lo stesso flusso di istruzioni a molti dati insieme invece di eseguire thread indipendenti. Su quale core gira un thread Java lo decide il sistema operativo.
Nella classifica TOP500 dei supercomputer (novembre 2025) tutti i sistemi usano Linux (dal 2017), e ci sono anche macchine italiane, come Leonardo a Bologna.
Flynn classifica i sistemi di calcolo in base al numero di flussi di istruzioni e di flussi di dati:
| Un flusso di dati | Più flussi di dati | |
|---|---|---|
| Un flusso di istruzioni | SISD: la macchina di von Neumann, il processore singolo | SIMD: la stessa istruzione diffusa a più unità, ognuna sui propri dati; parallelismo a grana fine, processori vettoriali, GPU |
| Più flussi di istruzioni | MISD: nessun sistema noto vi rientra | MIMD: ogni processore ha le proprie istruzioni e i propri dati; è il caso generale, e quello dei programmi di questo corso |
I sistemi MIMD si dividono secondo l'organizzazione della memoria, e da qui vengono i due modelli di programmazione del corso:
Lo speedup confronta il tempo T1 dell'algoritmo sequenziale con il tempo TN della versione parallela su N processori:
S = T1 / TN
Si vuole S > 1. Con S = 1 non si guadagna nulla: o il problema è intrinsecamente sequenziale, o il programma lo è diventato, per esempio perché la sincronizzazione serializza i thread. Il massimo teorico è S = N (speedup lineare): il lavoro si divide perfettamente e ognuno ne fa un N-esimo. In casi particolari si osserva S > N (super-lineare), di solito per effetto delle cache. Nel primo assignment lo speedup si misura su una macchina fissa, facendo variare il numero di thread.
Sia P la frazione del programma parallelizzabile perfettamente (tra 0 e 1) e 1 − P quella che resta sequenziale. Con N processori la parte sequenziale impiega ancora (1 − P)·T1, quella parallela P·T1/N. Sostituendo TN e semplificando T1 si ottiene lo speedup massimo:
S = 1 / ((1 − P) + P/N)
Basta una piccola parte sequenziale per limitare molto le prestazioni, perché aggiungere processori accelera solo la parte parallelizzabile. E spesso la parte sequenziale serve alla correttezza: sezioni critiche e sincronizzazione (capitoli 4–7) serializzano i thread, e un lock usato male può annullare il vantaggio di avere molti core.
Oltre a speedup ed efficienza, il calcolatore mostra quanti processori servono per arrivare al 90% del tetto: 9P/(1 − P), cioè 81 per P = 0,9 e 171 per P = 0,95. Oltre, ogni processore in più rende pochissimo.
Lo speedup non dice quanti processori è costato. L'efficienza lo normalizza:
E = S / N
Il valore ideale è 1, con tutti i processori usati a piena capacità, ma di solito è più basso. Uno speedup di 8 è buono su 8 core (E = 1), mediocre su 64 (E = 0,125).
Un tempo il collo di bottiglia era l'I/O su disco; nei multi-core è la memoria stessa. Nell'architettura più semplice tutti i core raggiungono la RAM attraverso un unico bus, sul quale passa un'operazione di memoria alla volta, anche se i thread accedono a indirizzi diversi e non hanno nulla da sincronizzare. Per questo contano le cache: un core che lavora sui dati in cache non passa dal bus. Di qui i protocolli di coerenza delle cache, sempre più complessi, e le architetture NUMA. Un programma parallelizzabile al 100% può così mancare lo speedup atteso per come usa la memoria.
Aumentare le prestazioni significa usare al meglio le CPU disponibili: nessun core deve restare fermo se c'è lavoro da fare. Quando un thread si blocca sull'I/O, il sistema operativo lo sospende e ne libera il core, che lavora solo se c'è un altro thread pronto (nella ready queue). Di qui la distinzione:
La formula che dimensiona un pool di thread in base al rapporto tra attesa e calcolo è nel capitolo 10. I thread comunque non sono gratuiti: ognuno occupa memoria per lo stack e crearlo coinvolge il sistema operativo, quindi si creano una volta e si riusano.
Ricci ha chiesto la legge di Amdahl, e che cosa succede con P = 0 e P = 1. Risposta: P è la frazione parallelizzabile. Con P = 0 lo speedup è 1 (nessun guadagno), con P = 1 è N (lineare), quindi lo speedup massimo va da 1 a N. Non va da 0 a 1: quello, semmai, è l'intervallo tipico dell'efficienza.
Un programma concorrente si scrive in un linguaggio concorrente e gira su una macchina concorrente. Quest'ultima fornisce tanti processori virtuali quanti sono i processi della computazione, e tre gruppi di meccanismi di base:
La macchina concorrente può essere logica: la JVM con i virtual thread esegue decine di migliaia di thread logici su tanti thread del sistema operativo quanti sono i core, e un runtime ad attori esegue milioni di attori su pochi thread. È la macchina a mappare le entità logiche sull'hardware e a offrire le primitive di interazione: lock, semafori, messaggi.
Un linguaggio concorrente deve permettere di specificare la concorrenza, cioè creare più processi, e l'interazione: sincronizzazione, comunicazione e mutua esclusione. Ci sono tre approcci di progetto:
| Approccio | Esempi |
|---|---|
| Linguaggio sequenziale + libreria di primitive concorrenti | C + PThreads |
| Linguaggio progettato per la concorrenza, con il processo come astrazione di prima classe | Occam, Ada, Erlang, Go |
| Ibrido: paradigma sequenziale esteso con un supporto nativo, più librerie e pattern costruiti sui meccanismi di base | Java e Scala, con java.util.concurrent |
I linguaggi mainstream offrono i processi soprattutto come libreria, ma Java non del tutto: synchronized è una parola chiave che in un linguaggio sequenziale non avrebbe senso. I thread implementano la nozione astratta di processo, e sono detti processi leggeri per distinguerli dai processi «pesanti» del sistema operativo, che sono programmi in esecuzione con uno o più thread. Oltre i thread, le slide elencano: framework a task come l'Executor di Java (capitolo 10), programmazione asincrona a event loop (capitolo 12), attori e active object, cioè OOP concorrente (capitolo 15), processi con message passing sincrono (capitolo 14), programmazione reattiva come FRP e Reactive Extensions (capitolo 13), e il calcolo parallelo su GPU o per i Big Data, fuori dal corso.
Un insieme finito di programmi sequenziali le cui esecuzioni possono sovrapporsi nel tempo e interagire. Un processo è l'esecuzione di uno di questi programmi sequenziali, cioè un flusso di controllo astratto: non necessariamente un processo del sistema operativo.
Perché non si può fare alcuna ipotesi sulla velocità relativa dei processi, quindi l'ordine in cui le loro azioni si intrecciano cambia da un'esecuzione all'altra. Lo stesso input può produrre esecuzioni e risultati diversi.
La concorrenza è un modo di strutturare il programma come attività sovrapposte che interagiscono (livello logico). Il parallelismo è l'esecuzione simultanea su processori fisici distinti, con l'obiettivo delle prestazioni. Sì: con una CPU il sistema operativo alterna i processi, che sono concorrenti ma non paralleli.
Nella multi-core ci sono più core general purpose sullo stesso chip, che condividono la RAM ed eventualmente la cache, al più di velocità diverse (P-core ed E-core). In quella eterogenea il processore è affiancato da unità specializzate di natura diversa, come GPU, NPU o FPGA.
SISD, SIMD, MISD (vuota in pratica) e MIMD. I MIMD si dividono in sistemi a memoria condivisa (SMP e NUMA), dove si comunica tramite variabili condivise, e sistemi a memoria distribuita (MPP, cluster, grid), dove si comunica con messaggi.
Lo speedup massimo con N processori è 1/((1 − P) + P/N), dove P è la frazione parallelizzabile. Con P = 0 vale 1, con P = 1 vale N. Per N → ∞ tende a 1/(1 − P): la parte sequenziale fissa un tetto che nessun numero di processori supera.
È E = S/N, lo speedup diviso per il numero di processori. Il valore ideale è 1. Serve perché lo speedup da solo non dice a che prezzo è stato ottenuto: S = 8 con 64 processori significa E = 0,125.
Perché i core condividono il bus verso la RAM, sul quale passa un'operazione alla volta, anche quando accedono a indirizzi diversi. Cache, protocolli di coerenza e architetture NUMA servono ad attenuare il problema.
Le prestazioni dipendono da quanto si usano i core disponibili. Un thread bloccato sull'I/O lascia libero il suo core, che resta fermo se non c'è un altro thread pronto. Per il calcolo puro bastano circa tanti thread quanti core (più uno), mentre con molto I/O servono più thread dei core.
Tanti processori virtuali quanti sono i processi, con i meccanismi per la multiprogrammazione (creazione e scheduling), per la sincronizzazione e comunicazione (a memoria condivisa o a messaggi) e per il controllo dell'accesso alle risorse. Può essere logica, come la JVM con i virtual thread.
Slide Course Introduction e [module-1.1] Concurrent Programming — Introduction · 16 febbraio 2026, prima parte (§1–6, 9: il corso, definizioni, hardware, Flynn, Amdahl, linguaggi e macchine) e seconda parte (§1–4, 7: ripresa di definizioni, Amdahl, Flynn, paradigma, macchine) · 20 febbraio (§1, 5: esame, speedup e Amdahl, non determinismo, macchina concorrente) · 23 febbraio (§1–3, 6: avvisi sull'esame, prestazioni, CPU-bound e I/O-bound). La mappa completa è in coverage-matrix.json.