Parte I — Concorrenza a memoria condivisa · Capitolo 1

Introduzione: concorrenza, parallelismo e architetture

~28 min di lettura5 widget interattivi

In questo capitolo

  1. Il corso e come studiarlo
  2. Concorrenza: definizioni e concetti fondamentali
  3. Concorrente, parallelo, distribuito
  4. La programmazione concorrente come paradigma
  5. Evoluzione hardware: il "Free Lunch is Over"
  6. Tassonomia di Flynn e architetture parallele
  7. Misurare le performance: speedup, efficienza e legge di Amdahl
  8. CPU-bound vs IO-bound: esperimenti con i thread
  9. Linguaggi e macchine concorrenti
  10. Verifica le tue conoscenze
  11. Riferimenti bibliografici

1. Il corso e come studiarlo

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.

Nota del redattore

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

Modalita' d'esame

L'esame si compone di due macro-parti:

ParteDescrizione
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.
OraleColloquio 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.

Struttura del corso

  1. Parte 1 — Programmazione multi-threaded (Java, meccanismi di sincronizzazione)
  2. Parte 2 — Programmazione concorrente basata su messaggi (attori, Erlang, Go)
  3. Parte 3 — Programmazione distribuita: comunicazione su rete

Assignment: modalita' operative

Per l'esame

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.

Idea chiave

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.

Studenti lavoratori e registrazioni

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.

Per l'esame

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.

Cosa portiamo a casa dal corso

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.

Nota di fine corso

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.

AsseDomanda da saper rispondere
Actor modelPerche' elimina shared mutable state e riduce le race?
AsincroniaCome si coordina una risposta quando il mittente non aspetta?
Parte distribuitaCome 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.

2. Concorrenza: definizioni e concetti fondamentali

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.

Concorrenza: definizione generale

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.

Idea chiave

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.

Programmazione 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' fondamentali dei processi

Proprieta'Significato
Indipendenza dalla velocita'L'esecuzione dei processi e' completamente asincrona: non si possono fare assunzioni sulla loro velocita' relativa.
Non-determinismoL'ordine di interleaving delle istruzioni non e' deterministico, puo' variare a ogni esecuzione.

Esempio concreto: sistemi operativi

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.

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

3. Concorrente, parallelo, distribuito

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 punto di Rob Pike

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.

Citazione

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

4. La 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' 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.

5. Evoluzione hardware: il "Free Lunch is Over"

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.

Architetture multi-core

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.

Architetture ibride (P-core + E-core)

Le architetture recenti sono ibride, combinando core di dimensioni diverse:

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

Architetture eterogenee e many-core

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.

Supercomputer e cluster

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.

Per l'esame

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

6. Tassonomia di Flynn e architetture parallele

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.

Nota del redattore

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.

Tassonomia di Flynn

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

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

Modelli MIMD

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:

  • 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 alcuni blocchi sono fisicamente piu' vicini a certi processori, creando differenze di velocita' di accesso.

Ogni processo ha il proprio spazio di indirizzi. Comunicano tramite scambio di messaggi (inviare e ricevere messaggi). Sottoclassi:

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

In sintesi:

ModelloMemoriaComunicazione
Memoria condivisaSingolo spazio di indirizziLettura/scrittura di variabili condivise
Memoria distribuitaOgni processo ha il proprio spazioMessage passing (send/receive)

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.

7. Misurare le performance: speedup, efficienza e legge di Amdahl

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.

Idea chiave

Lo speedup misura quanto piu' veloce e' l'esecuzione parallela rispetto a quella sequenziale. L'efficienza misura quanto bene vengono sfruttati i processori disponibili.

Speedup

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.

Legge di Amdahl

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→∞"]
      

Esplora la legge di Amdahl

Trascina il cursore per variare la porzione parallelizzabile P e il numero di core N.

Speedup massimo teorico: 6.90

Si noti come, anche con P=0.95 (95% parallelizzabile), lo speedup massimo e' limitato a 20x. Con P=0.5, lo speedup non supera 2x qualsiasi sia il numero di core.

Il simulatore seguente aggiunge al calcolo anche l'efficienza e la curva dello speedup al variare di N:

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.

Attenzione

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.

Efficienza

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.

Il collo di bottiglia della memoria

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.

Fattori che influenzano le misure

Quando si misurano le performance di un programma concorrente, bisogna tenere conto di:

8. CPU-bound vs IO-bound: esperimenti con i thread

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.

CPU-bound test

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.

IO-bound test

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

Idea chiave

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.

Modello del worker nel test

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
        }
    }
}

Osservazione con JConsole

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.

9. 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 (fisici o virtuali).

Macchina concorrente

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.

Idea chiave

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:

  1. Multiprogrammazione: creazione e gestione dei processori virtuali, scheduling per allocare i processori fisici a quelli virtuali.
  2. Sincronizzazione e comunicazione: due modelli architetturali diversi:
    • Memoria condivisa: presenza di una memoria comune tra i processori virtuali (es. programmazione multi-thread).
    • Message passing: ogni processore virtuale ha la propria memoria; la comunicazione avviene tramite scambio di messaggi.
  3. Controllo d'accesso alle risorse.

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.

Approcci alla progettazione di linguaggi concorrenti

Il professor Ricci identifica tre approcci principali:

ApproccioEsempiDescrizione
Linguaggio sequenziale + libreriaC + PThreadsSi aggiungono primitive concorrenti tramite librerie esterne.
Linguaggio nativo per concorrenzaOCCAM, ADA, Erlang, GoIl linguaggio e' progettato fin dall'inizio per la concorrenza.
Approccio ibridoJava, ScalaParadigma 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.

Oltre i thread: scenario ricco

Il panorama della programmazione concorrente moderna va ben oltre i thread tradizionali:

Verifica le tue conoscenze

Nota del redattore

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.

Qual e' la differenza principale tra programmazione concorrente e programmazione parallela?

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.

Qual e' la differenza fondamentale tra concorrenza e parallelismo (secondo Rob Pike)?

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 si intende con "The Free Lunch is Over"?

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.

Cosa dice la legge di Amdahl?

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.

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

Qual e' la differenza tra MPP, cluster e grid computing?

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.

Cosa distingue la sincronizzazione dalla mutua esclusione?

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.

Spiegare il problema di Alice e Bob (race condition sul latte).

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.

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.

Cosa sono deadlock, starvation e livelock?

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

Quali sono le differenze tra deadlock, starvation e livelock (in dettaglio)?

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.

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.

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.

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.

Qual e' la regola fondamentale dell'event-driven programming?

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.

Cosa sono le Promise? Quali problemi risolvono e quali limiti hanno?

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.

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

Cosa sono le coroutine e come si relazionano ad async/await?

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.

Riferimenti bibliografici