Quattro strade, quattro quadranti, 97 auto come thread. Ogni auto deve prenotare tutti i quadranti del suo percorso in un colpo solo, attraversare e rilasciare. Qui sotto vedi il programma in funzione in tempo reale; scorri per la spiegazione completa di come funziona, lock compresi.
Il programma Python lancia 97 auto con arrivi scaglionati di 0,1–0,4 s; qui il flusso è continuo e regolabile dal pannello (o aggiungi auto a mano), così puoi alzare la contesa e vedere code, attese e notify_all() in azione. Le traiettorie sono una ricostruzione grafica: il codice Python descrive solo quali quadranti vengono occupati (tabella PERCORSI). Le durate di attraversamento restano nell'intervallo 0,5–1,5 s del codice, adattate alla lunghezza del percorso.
Il programma è una simulazione di concorrenza in stile "cinque filosofi": un incrocio a quattro strade (Nord, Sud, Est, Ovest) diviso in quattro quadranti (NE, NO, SE, SO) è una risorsa condivisa. Ogni auto è un thread: arriva da una direzione, deve attraversare l'incrocio compiendo una manovra (destra, dritto o sinistra), quindi occupare i quadranti che il suo percorso attraversa, uno alla volta esclusivi, attraversare e infine rilasciarli.
N
+---------+
| NO| NE |
O |----+----| E
| SO| SE |
+---------+
S
NO = Nord-Ovest NE = Nord-Est
SO = Sud-Ovest SE = Sud-Est
| Componente | Ruolo | Meccanismo di sincronizzazione |
|---|---|---|
Auto(Thread) | Una per ogni auto; nel suo run() chiama incrocio.attraversa(self) | È il thread: si blocca e si risveglia tramite la Condition dell'incrocio |
Incrocio | Contiene lo stato condiviso (libero, occupanti, _log) e i metodi di prenotazione/rilascio | Lock (big_lock) + Condition (cond_incrocio) |
PERCORSI | Dizionario (provenienza, svolta) → [quadranti]: definisce quali quadranti servono a ogni manovra | — (sola lettura, quindi condivisibile senza lock) |
VisualizzatoreIncrocio(Thread) | Thread che ogni 0,5 s stampa la dashboard: griglia dei quadranti + ultime righe di log | Usa big_lock indirettamente tramite stampa_stato(); ha un lock tutto suo per il proprio flag attivo |
main | Crea incrocio, avvia il visualizzatore, lancia 97 auto scaglionate di 0,1–0,4 s, fa join() di tutte, ferma il visualizzatore | — |
libero[q] = True/False (False = occupato).big_lock): nessun ordine di acquisizione, nessuna attesa circolare, nessun deadlock.Lock() è il mutex (si entra con with / acquire–release). Una Condition è sempre legata a un mutex: cond_incrocio = Condition(big_lock) significa che ogni wait() e ogni notify_all() devono avvenire mentre si tiene big_lock (Python solleva RuntimeError se notifichi senza il lock).
Cosa protegge big_lock: i booleani libero (la mutua esclusione vera), più occupanti e _log, che servono solo a mostrare uno stato coerente nella dashboard.
Tre cose importanti, tutte in queste poche righe:
all(...)) e la marcatura (libero[q] = False) stanno nella stessa sezione critica. Nessun altro thread può infilarsi tra "ho verificato che sono tutti liberi" e "li ho occupati": è ciò che elimina la corsa critica del check-then-act.while e non if. Quando vieni svegliato, non è detto che la condizione sia ancora vera: notify_all() sveglia tutti, ma uno solo (o nessuno) può vincere la gara; inoltre Python consente risvegli spuri. Il while fa ricontrollare sempre, dal punto in cui eri rimasto.wait() rilascia il lock mentre dorme. È la parte controintuitiva: wait() fa atomicamente "rilascia big_lock → dormi → riacquisisci big_lock prima di tornare". Se non lo facesse, nessun'altra auto potrebbe mai prenotare o rilasciare, e il sistema si bloccherebbe subito.notify_all() sveglia tutti i thread in attesa sulla Condition (non solo uno). Ognuno si risveglia, riacquisisce big_lock (uno alla volta), rifà il while: chi trova i suoi quadranti tutti liberi li prenota e prosegue, gli altri tornano a dormire. È un "risveglio collettivo e riprova"; con 97 auto si parla di thundering herd, ma su questa scala è del tutto innocuo.
Poiché anche notify_all() avviene sotto lo stesso mutex, non esiste il problema del lost wakeup: non può succedere che un'auto controlli "occupato", e il rilascio+notify le sfugga prima che si metta in wait().
attraversa() la time.sleep(...) che simula l'attraversamento avviene fuori dalla sezione critica. Le auto non "tengono" big_lock mentre passano: la mutua esclusione fisica sui quadranti è garantita dai booleani libero messi a False. Il lock serve solo a rendere atomica la transazione di prenotazione/rilascio. Risultato: due auto con percorsi disgiunti attraversano davvero in parallelo (es. chi va dritto da Nord usa NO+SO, chi svolta a destra da Sud usa solo SE: possono passare insieme).Un deadlock richiede che valgano tutte le condizioni di Coffman. Qui ne manca una fondamentale: hold-and-wait. Un'auto non prende mai "metà" delle risorse: o prenota tutte i quadranti che le servono (quando sono liberi contemporaneamente), o non ne prenota nessuno e aspetta. Senza possesso parziale non può formarsi nessun ciclo di attese.
| Cinque filosofi (classico) | Questo incrocio | |
|---|---|---|
| Risorse | 2 forchette a testa, prese una alla volta | 1–3 quadranti, prenotati tutti insieme |
| Attesa | Tiene la forchetta sinistra e aspetta la destra → hold-and-wait | Se non trova tutto libero, non tiene nulla → niente hold-and-wait |
| Deadlock | Possibile (attesa circolare) | Impossibile |
PERCORSI abbia un ordine (es. ['NO','SO','SE']) è solo descrittivo: il codice non richiede i quadranti uno alla volta in quell'ordine, li tratta come un insieme. Per questo non serve nessun "ordine globale di acquisizione" per evitare deadlock.big_lock, e quando| Metodo | Prende big_lock? | Cosa fa / può bloccarsi? |
|---|---|---|
impegna_incrocio | Sì | Controlla e prenota; se non può, wait() (si sblocca, riacquisisce il lock al risveglio) |
rilascia_incrocio | Sì | Libera i quadranti e chiama notify_all() |
_segna_occupati | Sì | Solo per la dashboard (chi è disegnato in quel quadrante) |
log | Sì | Appende una riga in _log |
stampa_stato | Sì | Legge occupanti e _log e stampa mentre tiene il lock: la dashboard è uno snapshot coerente |
tempo_che_simula_attraversamento | No | time.sleep() fuori dalla sezione critica: è il cuore della concorrenza |
attraversa | No | Coordina le chiamate precedenti; non prende direttamente il lock |
notify_all() sveglia tutti e chi arriva "prima" a riacquisire il lock riprova per primo: un'auto che chiede 3 quadranti può essere scavalcata ripetutamente da auto che ne chiedono 1 e che li occupano per pochi istanti. In teoria è starvation (non deadlock: il sistema continua a progredire). L'unico modo per garantire l'ordine d'arrivo è una coda FIFO esplicita delle richieste — che questo esercizio, volutamente, non ha.mainincrocio = Incrocio() — crea lo stato condiviso: lock, condition, quadranti liberivisualizzatore.start() — parte il thread che ogni 0,5 s ridisegna la dashboardAuto con provenienza e svolta casuali — ancora ferme, non sono thread avviatia.start() + sleep(0.1–0.4 s) — le auto "arrivano" scaglionate nel tempofor a in auto: a.join() — il main aspetta che tutte abbiano attraversato e siano terminatevisualizzatore.ferma() + join() — il flag attivo va a False sotto il lock del visualizzatore, il loop esce e il thread finiscerun() → incrocio.attraversa(self) — qui inizia la "vita" dell'auto"Auto#n (N->sinistra) in attesa di bloccare ['NO', 'SO', 'SE']" — scritto sempre, anche se poi non aspetterà affattoimpegna_incrocio(quadranti) — se tutti liberi: prenota e va avanti; altrimenti wait() sulla Condition e ritenta al risveglio_segna_occupati(quadranti, id) + log "attraversa [...]" — i quadranti diventano visibili in dashboard come occupati da questa autotime.sleep(0.5–1.5 s) — l'attraversamento, FUORI dal lock: le altre auto possono proseguire su altri quadranti_segna_occupati(quadranti, None) → rilascia_incrocio(quadranti) — libera i booleani e notify_all()"ha lasciato l'incrocio" — il thread terminaSupponi che Auto#7 (da Nord, svolta a sinistra → NO, SO, SE) stia attraversando, quando arrivano:
SO: occupato → wait().SE, NE: SE è occupato → wait().Quando #7 finisce, libera NO, SO, SE e chiama notify_all(): entrambe si svegliano e riprovano. #8 trova SO libero, #9 trova SE e NE liberi: i due insiemi sono disgiunti, quindi passano entrambe contemporaneamente. Questo è il vantaggio di bloccare insiemi di quadranti: il throughput è alto finché i percorsi non si sovrappongono.
Se invece fosse presente anche Auto#10 (da Nord, sinistra → NO, SO, SE), al risveglio le tre auto competono: se #8 e #9 prenotano per prime, #10 torna a dormire e ritenta al prossimo rilascio. Nessun ordine garantito, nessun deadlock.
wait().La regola è quella italiana (guida a destra): un'auto tiene la corsia di destra rispetto al proprio senso di marcia. Con il Nord in alto:
| Provenienza | Corsia d'ingresso | Perché |
|---|---|---|
| da Nord (va verso Sud) | metà Ovest della strada verticale | il lato destro di chi scende è l'Ovest |
| da Sud (va verso Nord) | metà Est | il lato destro di chi sale è l'Est |
| da Est (va verso Ovest) | metà Nord della strada orizzontale | il lato destro di chi va verso Ovest è il Nord |
| da Ovest (va verso Est) | metà Sud | il lato destro di chi va verso Est è il Sud |
NO.NO (sopra) + SO (sotto).NO → SO → SE.PERCORSI| Manovra | Quadranti | Da Nord | Da Sud | Da Est | Da Ovest |
|---|---|---|---|---|---|
| destra | 1 | NO | SE | NE | SO |
| dritto | 2 | NO SO | SE NE | NE NO | SO NO |
| sinistra | 3 | NO SO SE | SE NE NO | NE SE SO | SO NO NE |
Curiosità: le diagonali di ogni manovra sono speculari rispetto al centro (N↔S, E↔O), per la simmetria dell'incrocio e della guida a destra.
Nel codice Python i quadranti sono solo booleani: il programma non disegna traiettorie. Nella simulazione qui sopra le traiettorie sono ricostruite geometricamente per rendere visibile l'occupazione: curve strette per le svolte a destra, rettilinei per "dritto", archi ampi per le svolte a sinistra. Sono disegnate in modo da attraversare gli stessi quadranti di PERCORSI; per le svolte a sinistra da Est e da Ovest la traiettoria naturale sfiora per pochi pixel il quadrante adiacente al centro dell'incrocio, ma l'occupazione mostrata (colore e etichetta) è esattamente quella decisa dal lock, cioè dalla tabella.
stampa_stato prende big_lock, quindi la dashboard legge occupanti e _log come uno snapshot coerente, senza lock separati per il display._lock_id) sul contatore di classe _contatore: due esempi di "stato condiviso, un lock".notify_all() è chiamato mentre si tiene il lock, e ogni attesa è dentro un while che ricontrolla.notify_all + "chi riprova per primo vince" non garantisce l'ordine d'arrivo: un'auto con 3 quadranti può essere scavalcata all'infinito (starvation teorica). Una coda FIFO di richieste sarebbe il rimedio, ma cambia il design.occupanti non è la verità, è cosmetica. La verità per la mutua esclusione è libero[]; occupanti[] è aggiornato in due momenti (dopo la prenotazione, prima del rilascio) solo per il disegno. Tra le due chiamate la dashboard può mostrare un'attimo di "libero" pure essendo il quadrante bloccato (per libero[]).stampa_stato stampa mentre tiene big_lock: con un terminale lento (o refresh continuo) questo allunga la sezione critica e può rallentare le auto. Lo stile "professionale" sarebbe copiare i dati sotto lock e stampare fuori. Il commento nel codice spiega che è una scelta: un solo lock, niente copie._log cresce senza limite (in un esercizio va benissimo; in un programma lungo servirebbe un buffer circolare).time.sleep(0.5–1.5) rappresenta l'attraversamento: realistico come durata, ma non legato a nessuna fisica. Nessun timeout: un'auto può attendere indefinitamente (coerente con l'assenza di fairness).notify() o Condition separate per gruppi di quadranti) per ridurre i risvegli inutili.big_lock: l'unico mutex. Protegge tutto lo stato dell'incrocio.cond_incrocio: Condition agganciata a big_lock; le auto si mettono qui in attesa.libero: un booleano per quadrante (True = libero). È la variabile della mutua esclusione.occupanti e _log: stato accessorio per la dashboard, protetto dallo stesso lock.quadranti_necessari() (una lettura di PERCORSI, nessun lock necessario).impegna_incrocio: qui sta la sincronizzazione (può bloccarsi a lungo)._segna_occupati(..., id): i quadranti compaiono come occupati in dashboard.time.sleep: attraversamento simulato, fuori dal lock._segna_occupati(..., None) poi rilascia_incrocio: liberano i booleani e svegliano tutti.while è il pattern standard delle Condition: si ricontrolla sempre la condizione dopo ogni risveglio.wait() rilascia big_lock mentre dorme e lo riacquisisce al risveglio.for di prenotazione è nella stessa sezione critica del controllo: niente corsa critica.True in un'unica sezione critica.notify_all() sveglia tutti gli attesisti: ognuno riproverà il while di impegna_incrocio, uno alla volta (il lock è uno solo).Thread: il suo lavoro è nel run()._lock_id (esempio di stato condiviso tra istanze).run() fa una sola cosa: attraversa().self.lock), separato da big_lock: protegge solo il flag attivo, non lo stato dell'incrocio.intervallo secondi; stampa_stato prende big_lock internamente, quindi la coerenza delle letture è garantita dal lock dell'incrocio.ferma() imposta attivo = False; il thread esce al controllo successivo e il join() ritorna.big_lock: nessun lock separato per il display, letture sempre coerenti (è una scelta deliberata, spiegata nel commento del codice)._log_stampati come cursore.\033[2J (cancella schermo) e \033[H (cursore in alto a sinistra) per ridisegnare la griglia a ogni refresh, mostrando solo le ultime _MAX_LOG = 8 righe.join di tutte → stop del visualizzatore.sleep(0.1–0.4) tra uno start() e l'altro simula arrivi distanziati nel tempo.join() di tutte le auto è ciò che rende print("Simulazione terminata...") attendibile: a quel punto nessun thread auto è vivo.