Concorrenza in Java - thread, sincronizzazione e pool
In questa pagina 8
I concetti generali di thread, race condition, mutua esclusione e sincronizzazione sono in Thread e concorrenza - modelli, Pthreads e goroutineUn thread è un flusso di esecuzione dentro un processo: ha program counter, registri e stack propri ma condivide codice, dati, heap e file aperti del processo, quindi costa meno di un processo (creazione e commutazione) e comunica più facilmente, ma i dati condivisi producono race condition; concorrenza (più compiti che avanzano, anche su un solo core) non è parallelismo (più compiti eseguiti nello stesso istante, serve più di un core); si distinguono parallelismo sui dati e sui compiti e la legge di Amdahl dà lo speedup massimo 1/(S+(1-S)/N) con S frazione seriale; i thread utente sono gestiti da una libreria, quelli kernel dal sistema, e i modelli many-to-one, one-to-one (Linux, Windows) e many-to-many li collegano; in C si usa Pthreads (pthread_create, pthread_join, pthread_exit), in Go le goroutine (parola chiave go, sync.WaitGroup, canali); questioni aperte: fork/exec con i thread, segnali, cancellazione (asincrona o differita), thread-local storage, contention scope (PCS e SCS).Thread e concorrenza - modelli, Pthreads e goroutine → e in Semafori, monitor e problemi classici di sincronizzazioneUn semaforo è un intero accessibile solo con due operazioni atomiche, wait (P: aspetta finché S > 0 e decrementa) e signal (V, in POSIX sem_post: incrementa); il semaforo contatore ha valori illimitati, quello binario vale 0 o 1; con valore iniziale 1 realizza la mutua esclusione, con valore iniziale 0 impone un ordine di esecuzione (S1 prima di S2); senza attesa attiva ogni semaforo ha una coda di processi sospesi (sleep e wakeup); i semafori si usano male con facilità (ordine sbagliato, omissioni); il monitor è un tipo di dato astratto in cui un solo processo alla volta è attivo e le variabili condizione x.wait()/x.signal() sospendono e risvegliano (in POSIX mutex più pthread_cond_t); le anomalie di liveness sono deadlock, starvation e inversione di priorità; i problemi classici sono produttore-consumatore a buffer limitato (mutex, vuote = n, piene = 0; l'ordine delle wait è importante), lettori-scrittori (rw_mutex, mutex, read_count; i lettori possono far morire di fame gli scrittori) e filosofi a cena (la soluzione ingenua con un semaforo per bacchetta va in deadlock; si evita limitando a n-1 filosofi, rompendo la simmetria o ordinando le risorse), con soluzioni in C (POSIX) e in Go (canali e sync).Semafori, monitor e problemi classici di sincronizzazione →: questa nota li traduce in Java e li usa per gli esempi del corso (MyThread1, MyThread2, MyPool) e per i pattern concorrenti del programma. Tutti i programmi sono stati compilati ed eseguiti con JDK 21; l'ordine di esecuzione dei thread dipende dallo scheduler, quindi nelle uscite si riportano solo le parti deterministiche.
Creare e avviare thread
Definizione. Un thread Java è un flusso di esecuzione di un oggetto
Thread; il codice che esegue è il metodorun()di unRunnable. Tutti i thread di una JVM condividono lo stesso heap (ognuno ha il proprio stack).
Due modi equivalenti: ereditare da Thread e ridefinire run(), oppure (preferibile: si può ancora ereditare da altro) implementare Runnable e passarlo a un Thread:
Runnable lavoro = () -> System.out.println("ciao da " + Thread.currentThread().getName());
Thread t = new Thread(lavoro, "t1");
t.start(); // crea il thread: la JVM chiama run() nel NUOVO thread
t.join(); // il chiamante aspetta la fine del thread
// lavoro.run() chiamato direttamente eseguirebbe il codice nel thread corrente, senza concorrenza- Il programma termina quando finiscono tutti i thread non daemon; il
mainpuò finire prima degli altri (il corso stampa "terminates" subito, senza aspettare: manca lajoin). join()aspetta la fine di un thread,sleep(ms)sospende il thread corrente (lanciaInterruptedException),yield()suggerisce allo scheduler di passare ad altri,interrupt()chiede a un thread di fermarsi: i metodi bloccanti reagiscono lanciandoInterruptedException, e chi la cattura non deve ingoiarla (Thread.currentThread().interrupt()per rimettere il flag).setDaemon(true)(prima distart) rende il thread "di servizio": non impedisce la chiusura del programma.
L'esempio del corso MyThread2
MyThread2 implementa Runnable e crea un thread per ogni argomento della riga di comando, tutti sullo stesso oggetto (new Thread(this, argv[i])): quindi il campo k è condiviso. Nella versione sotto (con join e meno giri) la stampa e l'incremento di k stanno dentro synchronized(this), così i valori stampati sono sempre 0, 1, 2... senza buchi né ripetizioni, anche se i nomi dei thread si alternano in modo diverso a ogni esecuzione. MyThread1, senza campo condiviso, mostra solo l'intreccio delle stampe.
/** Esempio del corso (MyThread2): piu' thread eseguono lo stesso Runnable e condividono il campo k. */
public class MyThread2 implements Runnable {
int k = 0; // condiviso: tutti i thread usano lo stesso oggetto (this)
public static void main(String[] argv) throws InterruptedException {
new MyThread2().exec(argv);
}
public void exec(String[] argv) throws InterruptedException {
Thread[] t = new Thread[argv.length];
for (int i = 0; i < argv.length; i++) {
t[i] = new Thread(this, argv[i]); // stesso Runnable, nome diverso
t[i].start(); // start() crea il thread e chiama run() in esso; run() direttamente NO
}
for (Thread x : t) x.join(); // il corso non fa join: il main termina prima degli altri
System.out.println(Thread.currentThread().getName() + " termina, k = " + k);
}
public void run() {
for (int i = 0; i < 4; i++) {
synchronized (this) { // lettura, stampa e incremento di k come un'unica sezione critica
System.out.println("thread " + Thread.currentThread().getName() + " val " + k);
k = k + 1;
}
Thread.yield(); // suggerisce allo scheduler di far girare un altro thread
}
}
}Uscita con argomenti A B C (i nomi cambiano tra le esecuzioni, la sequenza di val no):
thread A val 0
thread A val 1
thread A val 2
thread A val 3
thread B val 4
thread B val 5
thread B val 6
thread C val 7
thread B val 8
thread C val 9
thread C val 10
thread C val 11
main termina, k = 12Race condition e mutua esclusione
senza++ sembra un'istruzione ma sono tre passi (leggi, somma, scrivi): se due thread li intrecciano, un aggiornamento va perso. Il programma sotto fa incrementare 200000 volte lo stesso contatore da due thread in tre modi.
import java.util.concurrent.atomic.AtomicInteger;
/** Due thread incrementano lo stesso contatore: senza sincronizzazione si perdono aggiornamenti. */
public class CorsaCritica {
static int senza = 0; // nessuna protezione
static int con = 0; // protetto da synchronized
static final Object lucchetto = new Object();
static final AtomicInteger atomico = new AtomicInteger();
static final int GIRI = 200_000;
public static void main(String[] args) throws InterruptedException {
Runnable lavoro = () -> {
for (int i = 0; i < GIRI; i++) {
senza++; // lettura, somma, scrittura: 3 passi separabili
synchronized (lucchetto) { con++; }
atomico.incrementAndGet(); // un'unica operazione atomica
}
};
Thread t1 = new Thread(lavoro, "t1");
Thread t2 = new Thread(lavoro, "t2");
t1.start();
t2.start();
t1.join(); // il main aspetta la fine dei due thread
t2.join();
System.out.println("atteso = " + 2 * GIRI);
System.out.println("senza sync = " + senza + (senza == 2 * GIRI ? "" : " <-- aggiornamenti persi"));
System.out.println("synchronized= " + con);
System.out.println("AtomicInteger= " + atomico.get());
}
}Uscita (il valore senza sincronizzazione cambia a ogni esecuzione, ma è quasi sempre inferiore all'atteso):
atteso = 400000
senza sync = 373712 <-- aggiornamenti persi
synchronized= 400000
AtomicInteger= 400000| meccanismo | come funziona | quando |
|---|---|---|
synchronized (metodo o blocco) |
ogni oggetto ha un monitor (lock intrinseco); un solo thread alla volta lo possiede; è rientrante (lo stesso thread può riacquisirlo) | sezioni critiche brevi; il blocco synchronized(obj) permette di scegliere l'oggetto-lucchetto |
AtomicInteger, AtomicLong, ... |
operazioni atomiche senza lock (istruzioni dell'hardware) | contatori e flag semplici |
ReentrantLock (java.util.concurrent.locks) |
lock esplicito con lock()/unlock() (in finally), tryLock, condizioni |
quando servono attese con tempo massimo o più condizioni |
volatile |
garantisce la visibilità (un thread vede la scrittura di un altro) ma non l'atomicità di x++ |
flag di arresto, campo del double-checked locking |
oggetti immutabili (final, String) |
nessuno li modifica: nessun problema | dati condivisi in sola lettura |
Regole pratiche: proteggere ogni accesso allo stesso dato con lo stesso lock (leggere senza lock un dato scritto con lock è comunque un errore di visibilità); tenere le sezioni critiche corte; non chiamare codice sconosciuto mentre si tiene un lock.
Attesa di una condizione: wait e notifyAll
Quando un thread deve aspettare che un'altra condizione diventi vera (una copia disponibile, un posto libero nel buffer) non deve fare busy waiting: usa wait(), che rilascia il monitor e sospende il thread finché un altro chiama notify() o notifyAll() sullo stesso oggetto.
Regole (se si violano, si hanno errori sottili):
wait,notify,notifyAllsi chiamano solo dentrosynchronizedsullo stesso oggetto (altrimentiIllegalMonitorStateException);waitva sempre in un ciclowhileche ricontrolla la condizione: dopo il risveglio un altro thread può averla resa di nuovo falsa (e ci sono risvegli spuri);- in caso di dubbio
notifyAll(connotifysi risveglia un solo thread, a caso, che potrebbe non essere quello giusto); waitlanciaInterruptedException.
Il pool di risorse (MyPool del corso)
Il file MyPool.java del corso contiene un monitor Pool con get(n) (attende se n > risorse disponibili) e release(n) (restituisce e sveglia). Il file non compila e va corretto in più punti: la classe Pool è in un file chiamato MyPool.java mentre è dichiarata public class Pool; i costruttori sono scritti public pool(...) (minuscolo: sembrano metodi senza tipo di ritorno); this.wait() lancia InterruptedException, che è controllata e va dichiarata o catturata; pool += howMany(); chiama come metodo un parametro int; e currentPool legge pool senza sincronizzazione. Versione corretta (prendi e rilascia), con cinque lettori e due copie:
import java.util.concurrent.atomic.AtomicInteger;
/** Monitor: un insieme di risorse identiche (per esempio le copie di un libro). */
class Pool {
private int disponibili;
Pool(int iniziali) { disponibili = iniziali; }
/** Prende n risorse; se non bastano, il thread attende. */
synchronized void prendi(int n) throws InterruptedException {
while (n > disponibili) { // while, mai if: dopo il risveglio si ricontrolla
wait();
}
disponibili -= n;
}
/** Restituisce n risorse e sveglia chi aspetta. */
synchronized void rilascia(int n) {
disponibili += n;
notifyAll();
}
synchronized int disponibili() { return disponibili; }
}
public class PoolRisorse {
static final AtomicInteger dentro = new AtomicInteger();
static final AtomicInteger massimo = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Pool pool = new Pool(2);
Thread[] lettori = new Thread[5];
for (int i = 0; i < lettori.length; i++) {
final int id = i;
lettori[i] = new Thread(() -> {
try {
pool.prendi(1); // al massimo 2 lettori contemporanei
massimo.accumulateAndGet(dentro.incrementAndGet(), Math::max);
Thread.sleep(20); // il lettore "usa" la copia
dentro.decrementAndGet();
pool.rilascia(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "lettore-" + id);
lettori[i].start();
}
for (Thread t : lettori) t.join();
System.out.println("lettori contemporanei al massimo: " + massimo.get());
System.out.println("copie disponibili alla fine: " + pool.disponibili());
}
}Uscita:
lettori contemporanei al massimo: 2
copie disponibili alla fine: 2Al massimo due lettori sono dentro insieme, e al termine le due copie sono tornate nel pool: la condizione n > disponibili è l'invariante del monitor.
Produttore-consumatore
Schema classico: un buffer limitato, i produttori attendono se è pieno, i consumatori se è vuoto. Un unico monitor (il buffer) con wait/notifyAll; il produttore segnala la fine con un valore speciale ("pillola avvelenata").
import java.util.LinkedList;
import java.util.Queue;
/** Buffer limitato con wait/notifyAll: i produttori attendono se e' pieno, i consumatori se e' vuoto. */
class Buffer<T> {
private final Queue<T> coda = new LinkedList<>();
private final int capacita;
Buffer(int capacita) { this.capacita = capacita; }
synchronized void metti(T x) throws InterruptedException {
while (coda.size() == capacita) wait();
coda.add(x);
notifyAll(); // sveglia i consumatori (e gli altri produttori)
}
synchronized T prendi() throws InterruptedException {
while (coda.isEmpty()) wait();
T x = coda.remove();
notifyAll();
return x;
}
}
public class ProduttoreConsumatore {
static final int FINE = -1; // "pillola avvelenata": dice al consumatore di fermarsi
public static void main(String[] args) throws InterruptedException {
Buffer<Integer> buf = new Buffer<>(3);
long[] somma = new long[1];
Thread produttore = new Thread(() -> {
try {
for (int i = 1; i <= 100; i++) buf.metti(i);
buf.metti(FINE);
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
Thread consumatore = new Thread(() -> {
try {
int x;
while ((x = buf.prendi()) != FINE) somma[0] += x;
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
produttore.start();
consumatore.start();
produttore.join();
consumatore.join();
System.out.println("somma consumata = " + somma[0] + " (atteso 5050)");
}
}somma consumata = 5050 (atteso 5050)In pratica si usa java.util.concurrent.ArrayBlockingQueue/LinkedBlockingQueue, che fanno già tutto questo (put e take bloccanti).
Stallo (deadlock)
Definizione. Due o più thread sono in stallo quando ciascuno aspetta un lock posseduto da un altro, e nessuno può procedere.
Le condizioni per lo stallo sono quattro (mutua esclusione, possesso e attesa, assenza di prelazione, attesa circolare); in Java il caso tipico è l'ordine opposto di acquisizione di due lock. Nel programma t1 prende A poi B, t2 prende B poi A: ognuno ha il primo e aspetta il secondo. I thread sono daemon per poter chiudere il programma. Rimedio: un ordine globale dei lock (tutti prendono A prima di B).
/** Due thread prendono due lucchetti in ordine opposto: stallo (deadlock). Qui si evita ordinandoli. */
public class Stallo {
static final Object A = new Object(), B = new Object();
static void bloccante(Object primo, Object secondo, String nome) {
synchronized (primo) {
try { Thread.sleep(50); } catch (InterruptedException e) { return; } // forza l'intreccio
synchronized (secondo) {
System.out.println(nome + " ha preso entrambi");
}
}
}
public static void main(String[] args) throws InterruptedException {
boolean ordinato = args.length > 0 && args[0].equals("ordinato");
Thread t1 = new Thread(() -> bloccante(A, B, "t1"));
// t2: ordine inverso => stallo; ordine uguale a t1 => nessuno stallo
Thread t2 = new Thread(() -> { if (ordinato) bloccante(A, B, "t2"); else bloccante(B, A, "t2"); });
t1.setDaemon(true); // i thread bloccati non impediscono la chiusura del programma
t2.setDaemon(true);
t1.start();
t2.start();
t1.join(1000);
t2.join(1000);
System.out.println(t1.isAlive() || t2.isAlive() ? "STALLO: i thread sono ancora bloccati" : "terminati");
}
}Uscita senza argomenti (ordine opposto) e con l'argomento ordinato:
STALLO: i thread sono ancora bloccatit1 ha preso entrambi
t2 ha preso entrambi
terminatiThread pool e ExecutorService
Creare un thread per ogni compito è costoso e non limita il carico. Un thread pool riusa un numero fisso di thread che prelevano i compiti da una coda. Executors.newFixedThreadPool(n) crea n thread; submit accetta un Runnable o un Callable<T> (un compito che restituisce un valore e può lanciare eccezioni) e restituisce un Future<T>, il cui get() attende il risultato.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
/** Thread pool: pochi thread riutilizzati per molti compiti; Future restituisce il risultato. */
public class PoolDiThread {
// compito: contare i numeri primi in [da, a)
static int primi(int da, int a) {
int n = 0;
for (int x = Math.max(da, 2); x < a; x++) {
boolean p = true;
for (int d = 2; (long) d * d <= x; d++) if (x % d == 0) { p = false; break; }
if (p) n++;
}
return n;
}
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(4); // 4 thread per 8 compiti
List<Future<Integer>> parziali = new ArrayList<>();
for (int k = 0; k < 8; k++) {
final int da = k * 25_000, a = da + 25_000;
Callable<Integer> compito = () -> primi(da, a);
parziali.add(pool.submit(compito));
}
int totale = 0;
for (Future<Integer> f : parziali) totale += f.get(); // get() attende il risultato
pool.shutdown(); // niente nuovi compiti
pool.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("primi sotto 200000 = " + totale + " (sequenziale: " + primi(0, 200_000) + ")");
}
}primi sotto 200000 = 17984 (sequenziale: 17984)Con shutdown() il pool non accetta altri compiti e termina quando ha finito quelli accodati (awaitTermination attende); dimenticarlo lascia i thread vivi e il programma non termina.
Elementi di pattern concorrenti
Un esercizio completo di sincronizzazione su una risorsa limitata (le copie di un libro) è in Esercizio - concorrenza sulle copie di un libro con monitor e attesa limitata (progetto originale).
Domande d'esame
1. Perché contatore++ eseguito da due thread può dare un risultato sbagliato? Come si corregge?
Traccia. È lettura, somma e scrittura: se i passi di due thread si intrecciano, un aggiornamento si perde (race condition). Si corregge rendendo l'operazione una sezione critica (synchronized sullo stesso lock per ogni accesso) o usando AtomicInteger; volatile non basta perché non rende atomico l'incremento.
2. Perché wait() va chiamato in un while e dentro un blocco synchronized?
Traccia. wait rilascia il monitor e va chiamato mentre lo si possiede (altrimenti IllegalMonitorStateException). Dopo il risveglio un altro thread può aver reso falsa la condizione (o il risveglio è spurio): il while la ricontrolla. notifyAll evita di risvegliare il thread sbagliato.
3. Fare un esempio di stallo con due lock e dire come evitarlo.
Traccia. t1 prende A poi B, t2 prende B poi A: ognuno possiede un lock e aspetta quello dell'altro. Si evita con un ordine globale di acquisizione (sempre A poi B), oppure con tryLock a tempo, oppure riducendo i lock.
Versione ripasso
- Thread: oggetto
Threadche eseguerun()di unRunnable;start()crea il thread (chiamarerun()no);join()attende;sleep/yield/interrupt;setDaemon(true)prima distart. Tutti i thread condividono l'heap. Il programma termina quando finiscono i thread non daemon. Non ingoiareInterruptedException.
Thread t = new Thread(() -> System.out.println("ciao"), "t1");
t.start();
t.join();- Race condition:
x++= leggi, somma, scrivi: gli aggiornamenti si perdono (200000 incrementi per 2 thread: senza sync si ottiene meno di 400000, consynchronizedoAtomicIntegeresattamente 400000).
Runnable lavoro = () -> {
for (int i = 0; i < 200_000; i++) {
senza++; // 3 passi: aggiornamenti persi
synchronized (lucchetto) { con++; } // sezione critica: sempre 400000
atomico.incrementAndGet(); // atomico: sempre 400000
}
};- Regole di
wait: (1) dentrosynchronizedsullo stesso oggetto, altrimentiIllegalMonitorStateException; (2) in unwhile(altri thread possono rendere falsa la condizione, risvegli spuri); (3) nel dubbionotifyAll; (4) lanciaInterruptedException. - Meccanismi:
synchronized(monitor, rientrante), bloccosynchronized(obj),AtomicInteger,ReentrantLock(lock/unlockinfinally,tryLock),volatile(visibilità, non atomicità), oggetti immutabili. Stesso lock per ogni accesso; sezioni critiche corte. wait/notify: solo dentrosynchronizedsullo stesso oggetto;waitrilascia il monitor; sempre inwhile (!condizione) wait();;notifyAllnel dubbio.
synchronized void prendi(int n) throws InterruptedException {
while (n > disponibili) wait();
disponibili -= n;
}
synchronized void rilascia(int n) { disponibili += n; notifyAll(); }MyPooldel corso non compila: classePoolinMyPool.java; costruttoripublic pool(...);this.wait()senza gestireInterruptedException;pool += howMany();(parametro chiamato come metodo);currentPoolnon sincronizzato. Corretto sopra: con 5 lettori e 2 copie al massimo 2 lettori sono dentro insieme e alla fine le copie tornano 2.- Produttore-consumatore: buffer limitato con un solo monitor;
mettiattende se pieno,prendise vuoto, entrambinotifyAll; fine con "pillola avvelenata" (-1); somma 1..100 = 5050. In pratica:ArrayBlockingQueue(put/take).
synchronized void metti(T x) throws InterruptedException {
while (coda.size() == capacita) wait(); // pieno: il produttore aspetta
coda.add(x);
notifyAll();
}
synchronized T prendi() throws InterruptedException {
while (coda.isEmpty()) wait(); // vuoto: il consumatore aspetta
T x = coda.remove();
notifyAll();
return x;
}- Stallo:
t1prende A poi B,t2B poi A; condizioni: mutua esclusione, possesso e attesa, no prelazione, attesa circolare. Rimedio: ordine globale dei lock,tryLocka tempo. Il programma di prova stampaSTALLO: i thread sono ancora bloccaticon l'ordine opposto et1 ha preso entrambi,t2 ha preso entrambi,terminaticon l'argomentoordinato. - Thread pool:
Executors.newFixedThreadPool(n);submit(Callable<T>)restituisceFuture<T>;get()attende;shutdown()+awaitTermination(senzashutdownil programma non termina). Esempio: 8 compiti su 4 thread contano i primi sotto 200000: 17984, uguale al sequenziale.
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> primi(0, 25_000)); // Callable
int n = f.get(); // attende il risultato
pool.shutdown();- Strumenti a confronto:
synchronized= semplice, rientrante, rilascio automatico;ReentrantLock=tryLock, tempi massimi, più condizioni, maunlockinfinally;AtomicInteger= senza lock, per un solo dato;volatile= visibilità; immutabili = nessun problema. MyThread2: un thread per argomento sullo stessoRunnable(campokcondiviso); consynchronized(this)i valori sono 0, 1, 2... senza buchi anche se i nomi si alternano; il corso non fajoin, quindi ilmainfinisce prima.- Pattern concorrenti: monitor, produttore-consumatore, thread pool, future, Singleton thread-safe (Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e PrototypeI pattern creazionali isolano la creazione degli oggetti dal codice che li usa: Singleton garantisce un'unica istanza (costruttore privato, metodo statico getInstance; la versione lazy ingenua non è thread-safe, si correggono con synchronized, inizializzazione eager, double-checked locking con volatile, holder idiom o enum); Factory Method lascia alle sottoclassi la scelta della classe da istanziare; Abstract Factory crea famiglie coerenti di prodotti correlati tramite un'interfaccia di fabbrica; Builder costruisce passo passo oggetti complessi con campi facoltativi; Prototype crea oggetti clonando un prototipo (copia profonda delle parti mutabili); la Simple Factory non è un pattern GoF ma sposta il codice di creazione in una classe sola.Pattern creazionali - Singleton, Factory Method, Abstract Factory, Builder e Prototype →), Observer con
CopyOnWriteArrayList(Pattern comportamentali - Observer, Strategy, Command, Iterator e Template MethodI pattern comportamentali descrivono come gli oggetti collaborano e si dividono le responsabilità: Observer (publish-subscribe) avvisa automaticamente i dipendenti quando il soggetto cambia stato, mantenendo basso l'accoppiamento (si registrano e cancellano a runtime, modello push o pull, attenzione alle modifiche durante la notifica); Strategy rende intercambiabili gli algoritmi dietro un'interfaccia (in Java anche con una lambda); Command trasforma la richiesta in un oggetto, quindi memorizzabile, accodabile e annullabile (undo); Iterator scorre un aggregato senza esporne la struttura (for-each, fail-fast); Template Method fissa lo scheletro di un algoritmo e lascia alle sottoclassi passi astratti o hook (principio di Hollywood); State, Visitor, Chain of Responsibility, Mediator, Memento e Interpreter completano le undici forme del catalogo.Pattern comportamentali - Observer, Strategy, Command, Iterator e Template Method →), oggetti immutabili. Teoria: Thread e concorrenza - modelli, Pthreads e goroutineUn thread è un flusso di esecuzione dentro un processo: ha program counter, registri e stack propri ma condivide codice, dati, heap e file aperti del processo, quindi costa meno di un processo (creazione e commutazione) e comunica più facilmente, ma i dati condivisi producono race condition; concorrenza (più compiti che avanzano, anche su un solo core) non è parallelismo (più compiti eseguiti nello stesso istante, serve più di un core); si distinguono parallelismo sui dati e sui compiti e la legge di Amdahl dà lo speedup massimo 1/(S+(1-S)/N) con S frazione seriale; i thread utente sono gestiti da una libreria, quelli kernel dal sistema, e i modelli many-to-one, one-to-one (Linux, Windows) e many-to-many li collegano; in C si usa Pthreads (pthread_create, pthread_join, pthread_exit), in Go le goroutine (parola chiave go, sync.WaitGroup, canali); questioni aperte: fork/exec con i thread, segnali, cancellazione (asincrona o differita), thread-local storage, contention scope (PCS e SCS).Thread e concorrenza - modelli, Pthreads e goroutine →, Semafori, monitor e problemi classici di sincronizzazioneUn semaforo è un intero accessibile solo con due operazioni atomiche, wait (P: aspetta finché S > 0 e decrementa) e signal (V, in POSIX sem_post: incrementa); il semaforo contatore ha valori illimitati, quello binario vale 0 o 1; con valore iniziale 1 realizza la mutua esclusione, con valore iniziale 0 impone un ordine di esecuzione (S1 prima di S2); senza attesa attiva ogni semaforo ha una coda di processi sospesi (sleep e wakeup); i semafori si usano male con facilità (ordine sbagliato, omissioni); il monitor è un tipo di dato astratto in cui un solo processo alla volta è attivo e le variabili condizione x.wait()/x.signal() sospendono e risvegliano (in POSIX mutex più pthread_cond_t); le anomalie di liveness sono deadlock, starvation e inversione di priorità; i problemi classici sono produttore-consumatore a buffer limitato (mutex, vuote = n, piene = 0; l'ordine delle wait è importante), lettori-scrittori (rw_mutex, mutex, read_count; i lettori possono far morire di fame gli scrittori) e filosofi a cena (la soluzione ingenua con un semaforo per bacchetta va in deadlock; si evita limitando a n-1 filosofi, rompendo la simmetria o ordinando le risorse), con soluzioni in C (POSIX) e in Go (canali e sync).Semafori, monitor e problemi classici di sincronizzazione →. - Uscite dei programmi:
PoolRisorse:lettori contemporanei al massimo: 2,copie disponibili alla fine: 2;ProduttoreConsumatore:somma consumata = 5050 (atteso 5050);PoolDiThread:primi sotto 200000 = 17984 (sequenziale: 17984);CorsaCritica:atteso = 400000,senza syncquasi sempre meno (valore diverso a ogni esecuzione),synchronizedeAtomicInteger= 400000. InterruptedException: lanciata dasleep,wait,join; chi la cattura rimette il flag conThread.currentThread().interrupt()(non la ingoia).- Errori tipici:
ifal posto diwhileconwait;waitfuori dasynchronized; lock diversi per lo stesso dato; credere chevolatilerenda atomicox++; chiamarerun()invece distart(); non farejoin; dimenticareshutdown; acquisire i lock in ordine diverso (stallo).