Programmazione Concorrente e Distribuita — Prof. Alessandro Ricci

Introduzione alla Programmazione Concorrente e Programmazione Asincrona

2026-02-16104 minregistrazione originale

In questa lezione

1. Presentazione del corso

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

2. Concetti fondamentali

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, 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 di costruire programmi in cui molteplici attivita' computazionali si sovrappongono nel tempo e tipicamente interagiscono in qualche modo. Un programma concorrente e' un insieme finito di programmi sequenziali che possono essere eseguiti in parallelo.

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.

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

Rete. I processori sono distribuiti su una rete, senza memoria condivisa. La comunicazione avviene 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

4. 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. 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: Intel Core i7, AMD Ryzen Threadripper (fino a 64 core).

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 (8 P-core + 16 E-core). Anche AMD con l'architettura Zen 5 adotta un approccio simile, con chiplet design che collega piccole unita' (Core Complex Dies) tramite un'interconnessione ad alta velocita'.

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 (P-core + E-core), GPU, NPU per machine learning, e vari coprocessori.

Supercomputer e cluster

I supercomputer (es. Fugaku del RIKEN Center) 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).

5. 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 implementa questo modello.
MIMD (Multiple Instruction, Multiple Data)Ogni processore ha il proprio flusso di istruzioni e i propri dati.Maggior parte 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 accedono alla memoria comune 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. Sottoclassi:

  • MPP (Massively Parallel Processors): processori e rete strettamente accoppiati e specializzati. 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.

6. Legge di Amdahl e performance

Speedup

Lo speedup misura il miglioramento delle prestazioni quando si usa un algoritmo parallelo rispetto a quello sequenziale:

S = T1 / TN

T1 = tempo di esecuzione con 1 processore
TN = tempo di esecuzione con N processori
N  = numero di processori

Legge di Amdahl

La legge di Amdahl stabilisce un limite teorico allo speedup ottenibile parallelizzando un programma:

S = 1 / ((1 - P) + P/N)

P = proporzione del programma parallelizzabile
1 - P = parte non parallelizzabile (sequenziale)

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.

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). Una gestione inefficiente dei lock puo' vanificare i benefici di architetture multi-core.

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.

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. Da qui l'importanza della cache e dei protocolli di coerenza della cache, sempre piu' complessi e sofisticati.

7. Processi e interazione

Interazione tra processi

Qualsiasi programma concorrente non banale si basa su processi che interagiscono. I tipi fondamentali di interazione sono tre:

TipoNaturaDescrizione
CooperazionePrevista e desiderataI processi collaborano per un obiettivo comune. Include comunicazione (scambio di informazioni, tipicamente messaggi) e sincronizzazione (relazioni temporali tra processi, segnali temporali).
Competizione/ContentionPrevista ma non desiderataNecessaria per coordinare l'accesso a risorse condivise. Include mutua esclusione (regolare l'accesso a sezioni critiche) e critical sections (esecuzione concorrente di blocchi di azioni).
InterferenzaNON prevista ne' desiderataProduce effetti negativi solo quando il rapporto tra le velocita' dei processi assume valori specifici (errori tempo-dipendenti). Sono gli "heisen-bug" della programmazione concorrente.

Sincronizzazione vs mutua esclusione

Idea chiave

Il professor Ricci cita Buhr & Harji (2005): sostengono che "sincronizzazione = mutua esclusione" e' una leggenda metropolitana ancora presente in libri di testo e articoli di ricerca. I due concetti sono diversi:

Sincronizzazione

Definisce una relazione temporale tra processi: azioni che avvengono nello stesso momento, alla stessa velocita', o in una relazione di precedenza. Non richiede necessariamente dati condivisi.

Mutua esclusione

Definisce una restrizione sull'accesso a dati condivisi. E' priva di significato se non ci sono dati condivisi. La mutua esclusione richiede forme implicite di sincronizzazione (bloccare azioni, attendere altre).

8. Race conditions e situazioni critiche

Esempio classico: Alice, Bob e il latte

Il professor Ricci presenta il celebre esempio di Alice e Bob per illustrare le difficolta' della sincronizzazione. Alice e Bob vivono insieme e devono comprare il latte quando finisce:

Il problema

Entrambi controllano il frigo; se non c'e' latte e non c'e' un biglietto sul frigo, lasciano un biglietto, vanno a comprare il latte, lo mettono in frigo e rimuovono il biglietto. Il programma sembra funzionare ma in realta' e' soggetto a race conditions.

Race condition

Una race condition (o race hazard) si verifica quando due o piu' processi accedono e aggiornano concorrentemente risorse condivise, e il risultato dell'aggiornamento dipende dall'ordine specifico in cui avvengono gli accessi. Si manifesta in due forme principali:

  1. Cattiva gestione di interazioni previste
  2. Presenza di interazioni spurie non previste nel problema

Situazioni critiche

I problemi nei programmi concorrenti possono portare a tre situazioni critiche:

Deadlock (o "abbraccio mortale" secondo Dijkstra): situazione in cui due o piu' azioni (processi) sono in attesa che l'altra finisca, e nessuna delle due procede mai. Riguarda tipicamente il rilascio di una risorsa condivisa bloccata, la ricezione di un segnale temporale o un messaggio. Coinvolge piu' processi.

Esempio classico: Processo A tiene il lock su risorsa 1 e attende risorsa 2; Processo B tiene il lock su risorsa 2 e attende risorsa 1.

Starvation (o mancanza di equita'): situazione in cui un processo viene bloccato in un'attesa infinita. La starvation di risorse si verifica quando a un processo viene negato perpetuamente l'accesso alle risorse necessarie. Riguarda un singolo processo.

Esempio: Uno scheduler che favorisce sempre i processi ad alta priorita' e non concede mai CPU a quelli a bassa priorita'.

Livelock: simile al deadlock, ma gli stati dei processi coinvolti cambiano continuamente l'uno rispetto all'altro, senza che nessuno progredisca. E' un caso speciale di starvation: la definizione generale dice solo che un processo specifico non sta progredendo.

Esempio: Due processi che si passano continuamente una risorsa senza mai utilizzarla, in un "cortese" scambio infinito.

9. Linguaggi e macchine concorrenti

Macchina concorrente

Una macchina concorrente fornisce il supporto per eseguire programmi concorrenti, mettendo a disposizione tanti processori virtuali quanti sono i processi della computazione concorrente. Fornisce tre meccanismi di base:

  1. Multiprogrammazione: creazione e gestione di processori virtuali, scheduling dei processori fisici.
  2. Sincronizzazione e comunicazione: due modelli architetturali — memoria condivisa (multi-threaded) e message passing.
  3. Controllo d'accesso alle risorse.

Approcci alla progettazione di linguaggi concorrenti

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 (es. java.util.concurrent).

Oltre i thread: scenario ricco

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

10. Programmazione asincrona: event loop e callback

La programmazione asincrona e' uno stile sempre piu' importante, supportato ormai da tutti i principali framework e piattaforme (Java, .NET, iOS, Android, JavaScript/Node.js). Si tratta dell'esecuzione e gestione di computazioni/richieste/processi asincroni, astraendo dai thread.

Panorama attuale dell'async

Event-driven programming

Un programma e' fatto di gestori di eventi (event handler) che vengono chiamati (eseguiti, attivati) quando si verificano eventi. Gli eventi possono essere azioni utente (click, pressioni di tasti), output di sensori, messaggi, risposte ricevute. E' una "programmazione senza call stack": dopo l'esecuzione di ogni handler lo stack e' vuoto.

Modello di esecuzione: Event Loop

L'event loop e' un'architettura di controllo in cui un singolo thread di controllo attende eventi ed esegue gli handler corrispondenti. Una coda degli eventi tiene traccia degli eventi generati dall'ambiente o dagli handler stessi. Gli handler sono eseguiti atomicamente: se un evento si verifica mentre un handler e' in esecuzione, sara' servito solo al termine dell'handler corrente.

loop {
  Event ev = waitForEvent(eventQueue)
  Handler handler = selectHandler(ev)
  execute(handler)
}

Schema dell'Event Loop

Sorgenti eventi
Coda eventi
Event Loop
Handler
[Eventi: click, timer, I/O, message]

L'event loop estrae un evento dalla coda, seleziona l'handler registrato e lo esegue. Gli handler non devono mai bloccare.

Regola fondamentale: Never-blocking

Gli event handler non devono mai bloccare ne' contenere loop infiniti. Una chiamata bloccante bloccherebbe l'intero event loop, impedendo il processamento degli eventi in coda. Le chiamate bloccanti devono essere sostituite da richieste asincrone, che genereranno un evento al completamento.

Callback e Continuation Passing Style (CPS)

Le richieste asincrone usano il modello callback: una funzione (callback) viene specificata come argomento della chiamata asincrona. La callback viene invocata dall'event loop quando il risultato e' pronto. Questo definisce una continuazione della computazione, da cui Continuation-Passing Style (CPS).

Callback Hell / Pyramid of Doom

La composizione di callback annidate porta a un problema ben noto: callback hell (o "piramide della morte"). La struttura del codice si espande orizzontalmente invece che verticalmente, diventando difficile da leggere, mantenere ed estendere.

step1(function(result1) {
  step2(function(result2) {
    step3(function(result3) {
      // e cosi' via...
    })
  })
})

Esempio concreto discusso dal professore — la funzione loadUserPic in versione sincrona e asincrona:

Versione sincrona
function loadUserPic(userId) {
  let user = findUserById(userId);
  return loadPic(user.picId);
}
Versione asincrona (CPS)
function loadUserPic(userId, ret) {
  findUserById(userId, (user) => {
    loadPic(user.picId, ret);
  });
}
loadUserPic('john', (pic) => {
  ui.show(pic);
});

Nel server Node.js, l'annidamento e' ancora piu' pronunciato:

http.createServer((request, response) => {
  let uri = url.parse(request.url).pathname;
  let filename = path.join(process.cwd(), uri);
  path.exists(filename, (exists) => {
    if(exists) {
      fs.readFile(filename, (err, data) => {
        response.writeHead(200);
        response.end(data);
      });
    } else {
      response.writeHead(404);
      response.end();
    }
  });
}).listen(8080);
Idea chiave

Le callback sono spesso implementate come closure: un record che memorizza una funzione insieme all'ambiente lessicale in cui e' stata creata. Questo permette alla callback di accedere alle variabili del contesto originale quando viene invocata dall'event loop.

11. Promise e chaining

Il problema delle callback

Le callback hanno problemi noti: "asynchronous spaghetti" (frammentazione della computazione), "pyramid of doom" (annidamento), difficolta' di gestione degli errori. Le Promise sono state proposte per risolvere questi problemi.

Cos'e' una Promise?

Una Promise e' un oggetto proxy che rappresenta un risultato non ancora disponibile ma che sara' calcolato in futuro (simile ai futures, proposti nel 1976 da Friedman e Wise). Incapsula azioni asincrone, comportandosi come un valore di ritorno di una computazione, anche se il valore potrebbe non essere ancora disponibile.

Proprietà fondamentali

API delle Promise in JavaScript

Promise chaining

La caratteristica chiave per appiattire la "piramide della morte": il metodo then ritorna a sua volta una Promise, permettendo il chaining:

let promise = new Promise((resolve, reject) => {
  resolve(1);
});
promise.then((val) => {
  console.log(val); // 1
  return val + 2;
}).then((val) => {
  console.log(val); // 3
});

Composizione di Promise

L'API delle Promise fornisce due metodi fondamentali per la composizione:

MetodoComportamentoEsempio d'uso
Promise.all([...])Attende che tutte le Promise siano risolte (o una sia rigettata). Le callback ricevono un array con i valori.Eseguire piu' richieste indipendenti in parallelo e attendere tutte le risposte.
Promise.race([...])Completa non appena una qualsiasi Promise si risolve o viene rigettata.Timeout su una richiesta: chi arriva prima tra richiesta e timer.
// Promise.all - join point per task paralleli
let myProm1 = delayWithRand(1000);
let myProm2 = delayWithRand(2000);
let myTotalProm = Promise.all([myProm1, myProm2]);
myTotalProm.then((values) => {
  console.log(values[0]); // risultato del primo
  console.log(values[1]); // risultato del secondo
});

// Promise.race - chi arriva prima
let myProm1 = delayWithRand(1000);
let myProm2 = delayWithRand(2000);
let myTotalProm = Promise.race([myProm1, myProm2]);
myTotalProm.then((value) => {
  console.log(value); // risultato del piu' veloce
});

Limiti delle Promise

Problemi noti

12. Async/await e coroutine

L'evoluzione: async & await

L'estensione async/await e' la soluzione piu' recente e promettente nella programmazione asincrona. L'obiettivo e' fornire uno stile di programmazione sincrono mantenendo un core asincrono. Introdotta in ES2017 (JavaScript), implementata in .NET TAP (C#), Python, DART, Kotlin e molti altri.

L'idea fondamentale:

// Stile async con Promise
let p = asyncFunc(...);
p.then(onCompleteFunc, onErrorFunc)

// Stile async/await - sembra sincrono ma e' asincrono
let p = asyncFunc(...);
let res = await p;
// usa res

await sospende l'esecuzione della funzione finche' la Promise non e' risolta, ma non blocca il thread di controllo: il controllo viene ceduto (yield), salvando lo stato corrente della computazione.

Attenzione

await puo' essere usato solo all'interno di funzioni marcate come async. Non e' utilizzabile al livello top-level del programma: serve introdurre almeno una funzione main asincrona.

Esempio concreto

Benefici di async/await

Problemi di async/await

ProblemaDescrizione
await solo in asyncNon utilizzabile al livello top-level, serve una funzione async wrapper.
Event-loop semantics preservedUna funzione async non puo' riprendersi mentre il thread e' occupato ad eseguire altri handler.
Blocchi {…} non atomiciL'esecuzione del codice di un singolo blocco puo' estendersi su piu' iterazioni dell'event loop, aprendo a race condition.
ComposizioneServe mescolare async/await con l'API Promise per composizioni complesse (es. Promise.all).
Design clash sincrono/asincronoMescolare stili sincrono e asincrono richiede forte disciplina per evitare comportamenti difficili da comprendere.

Modularita' e incapsulamento

Un esempio discusso dal professore mostra come sia facile sbagliare se non si rispetta la "catena" async:

// Questo NON funziona come ci si aspetterebbe
function g() {
  console.log("started g");
  f(); // f e' async, ma g non la awaita
  console.log("done g");
}

// Versione corretta
async function g() {
  console.log("started g");
  await f();
  console.log("done g");
}

Coroutine

Le coroutine sono una generalizzazione del concetto di subroutine: permettono di sospendere e riprendere l'esecuzione. Sono il meccanismo di basso livello usato per implementare async/await. A differenza delle subroutine tradizionali (che hanno un solo punto di ingresso e terminano sempre), le coroutine possono avere multipli punti di ingresso e uscita tramite yield.

Idea chiave

Le coroutine sono un meccanismo di basso livello per implementare cooperative multitasking (non preemptive). Sono utilizzate come base per: cooperative tasks, eccezioni, event loop, iteratori, liste infinite, pipe. Non sono direttamente correlate all'event-driven programming.

// Esempio: producer-consumer cooperativo con coroutine
var q := new queue

coroutine produce
  loop
    while q is not full
      create some new items
      add the items to q
    yield to consume

coroutine consume
  loop
    while q is not empty
      remove some items from q
      use the items
    yield to produce

Coroutine in Kotlin

Kotlin e' un esempio moderno di linguaggio con supporto nativo alle coroutine. Concetti principali:

ConcettoDescrizione
Suspending computationsComputazioni che possono sospendere la loro esecuzione senza bloccare il thread in cui risiedono, permettendo al thread di essere usato per altre computazioni.
Coroutine dispatchersDeterminano su quale thread avviare o riprendere una coroutine.
Coroutine buildersasync() (quando serve un risultato), launch() (senza risultato), runBlocking() (ponte tra codice bloccante e suspendable).

Fiber e virtual threads

Le fiber sono thread leggeri che usano cooperative multitasking (a differenza dei thread, che usano preemptive multitasking). Le fiber condividono lo spazio di indirizzi come i thread. Possono essere implementate usando un singolo thread. A livello di astrazione:

Nota del redattore

Il professor Ricci cita Brian Goetz: i virtual threads forniscono una migliore modularita' e incapsulamento per processi logici e flussi di controllo rispetto all'async programming. Tuttavia, come i thread fisici, non offrono un trattamento first-class per eventi e computazioni reattive.

Esempio Kotlin discusso a lezione:

import kotlinx.coroutines.*

fun main(args: Array<String>) = runBlocking {
    println("before async call")
    val result = async {
        println("inside the async call")
        delay(1000)
        println("exiting the async call")
        100
    }
    println("after the async call, before greet")
    greetDelayed(200)
    println("after greet trigger")
    println("${result.await()}")
}

suspend fun greetDelayed(delayMillis: Long) {
    delay(delayMillis)
    println("Hello, World!")
}

Verifica le tue conoscenze

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.

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

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

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.

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.

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

Riferimenti bibliografici