Parte I — Concorrenza a memoria condivisa · Capitolo 1

Introduzione: concorrenza, parallelismo e architetture

~15 min di lettura1 calcolatore: la legge di Amdahl

In questo capitolo

  1. Come funzionano il corso e l'esame
  2. Che cos'è la concorrenza
  3. Concorrente, parallelo, distribuito
  4. Perché programmare in modo concorrente
  5. La giungla dell'hardware
  6. La tassonomia di Flynn e la memoria
  7. Misurare le prestazioni: speedup, Amdahl, efficienza
  8. Macchine e linguaggi concorrenti
  9. Verifica le tue conoscenze

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.

1. Come funzionano il corso e l'esame

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:

  1. Fondamenti della programmazione concorrente: modellazione, verifica, programmazione multi-thread e a task (capitoli 1–11).
  2. Programmazione asincrona: a eventi e reattiva (capitoli 12–13).
  3. Message passing: canali e attori (capitoli 14–16).
  4. Programmazione distribuita: algoritmi distribuiti e cenni di servizi (capitoli 17–19).

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.

Dalle slide: libri di testo

2. Che cos'è la concorrenza

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:

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.

3. Concorrente, parallelo, distribuito

LivelloChe cosa conta
Programmazione concorrentelogico: attività che si sovrappongono nel tempo e interagiscono, anche su un solo processorel'organizzazione del programma
Programmazione parallelafisico: le esecuzioni si sovrappongono perché girano su processori fisici distintile prestazioni
Programmazione distribuitai processori sono sparsi su una rete e non condividono la memoriacomunicazione 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'è.

4. Perché programmare in modo concorrente

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:

ParadigmaIdeaNel corso
Multi-threadstato condiviso, sincronizzazione con semafori e monitorcapitoli 4–7, 10
A messagginiente stato condiviso, interazione solo con messaggi, sincroni o asincronicapitoli 14–16
A eventiil flusso del programma è determinato da eventi: azioni dell'utente, sensori, messaggicapitolo 12
Asincronaazioni e richieste asincrone, dogma del never blocking, future e callbackcapitolo 12
Reattivail programma è organizzato attorno a flussi di dati e alla propagazione dei cambiamenticapitolo 13

5. La giungla dell'hardware

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.

ArchitetturaCaratteristicheEsempi dalle slide
Multi-corepiù core sullo stesso chip, che condividono la RAM e a volte livelli di cacheIntel Core i7 (2009); AMD Threadripper 3990X (2020), 64 core
Multi-core ibrideP-core veloci e affamati di energia, E-core più piccoli ed efficienti per i compiti in backgroundIntel 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)
Eterogeneeun 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'AIApple M5, system-on-chip con CPU, GPU, NPU e coprocessori; Cerebras CS-3
Supercomputermoltissimi processori, di solito senza memoria condivisa, collegati da reti costruite appostaFugaku (RIKEN, Fujitsu): 7,6 milioni di core
Cluster e gridnodi e reti commerciali (Gigabit Ethernet, InfiniBand); comunicano a messaggiSystem X (Virginia Tech)
Cloudcalcolo come servizio in rete (SaaS, PaaS, IaaS), pubblico o privatoAmazon 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.

6. La tassonomia di Flynn e la memoria

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

Un flusso di datiPiù flussi di dati
Un flusso di istruzioniSISD: la macchina di von Neumann, il processore singoloSIMD: la stessa istruzione diffusa a più unità, ognuna sui propri dati; parallelismo a grana fine, processori vettoriali, GPU
Più flussi di istruzioniMISD: nessun sistema noto vi rientraMIMD: 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:

7. Misurare le prestazioni: speedup, Amdahl, efficienza

Speedup

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.

La legge di Amdahl

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.

Calcolatore: prova i casi limite

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.

Efficienza

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

La memoria come collo di bottiglia

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.

Tenere occupate le CPU

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.

Per l'esame

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.

8. Macchine e linguaggi concorrenti

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:

ApproccioEsempi
Linguaggio sequenziale + libreria di primitive concorrentiC + PThreads
Linguaggio progettato per la concorrenza, con il processo come astrazione di prima classeOccam, Ada, Erlang, Go
Ibrido: paradigma sequenziale esteso con un supporto nativo, più librerie e pattern costruiti sui meccanismi di baseJava 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.

Verifica le tue conoscenze

Che cos'è un programma concorrente, e che cosa si intende per processo?

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é un programma concorrente è non deterministico?

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.

Che differenza c'è tra concorrenza e parallelismo? Si può avere concorrenza con una sola CPU?

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.

Che differenza c'è tra un'architettura multi-core e una a core eterogenei?

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.

Quali sono le quattro classi di Flynn, e come si dividono i sistemi MIMD?

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.

Che cosa dice la legge di Amdahl? Che cosa succede con P = 0 e con P = 1?

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.

Che cos'è l'efficienza, e perché serve oltre allo speedup?

È 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é la memoria diventa un collo di bottiglia nei multi-core?

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.

Perché conta tenere occupate le CPU, e che cosa cambia tra compiti CPU-bound e I/O-bound?

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.

Che cosa fornisce una macchina concorrente?

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.

Da quali lezioni viene questo capitolo

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.