Parte I — Concorrenza a memoria condivisa · Capitolo 3

Correttezza: safety, liveness, fairness e LTL

~10 min di letturatabelle di verità su tracce infinite

In questo capitolo

  1. Perché il testing non basta
  2. Safety e liveness
  3. Fairness
  4. La logica temporale lineare
  5. Specificare le proprietà in LTL
  6. Overtaking e k-bounded overtaking
  7. Verifica le tue conoscenze

Il capitolo in un paragrafo. Un programma concorrente è corretto se certe proprietà valgono in tutte le sue computazioni, anche infinite, e il testing ne controlla solo alcune. Le proprietà sono di due tipi: di safety («non succede mai niente di male», vera in ogni stato) e di liveness («prima o poi succede qualcosa di buono»). Molte liveness valgono solo se lo scheduler è equo, e la fairness ha tre gradi. La logica temporale lineare (LTL) permette di scrivere queste proprietà in modo rigoroso con gli operatori always, eventually, next e until. Con il weak until si esprime anche un limite ai sorpassi, il k-bounded overtaking.

1. Perché il testing non basta

Un programma sequenziale si controlla con i test: stesso input, stesso output, e se l'output è sbagliato si corregge e si rilancia. Un programma concorrente, con lo stesso input, può dare output diversi a seconda dello scenario, e ogni esecuzione ne segue probabilmente uno diverso: il debugging consueto non funziona. La correttezza di un programma concorrente, che spesso non termina, si definisce quindi con proprietà delle computazioni: condizioni che devono valere in ogni scenario possibile.

Testing, verifica, validazione

I test di safety controllano invarianti con asserzioni; quelli di liveness controllano che il sistema progredisca, e sono più difficili da impostare. Entrambi coprono solo una parte degli scenari, e il codice di test altera i tempi e può mascherare il bug (gli heisenbug). Il testing rivela la presenza di errori, non la loro assenza. Per dimostrarne l'assenza servono il model checking e le dimostrazioni induttive di invarianti (capitolo 11).

2. Safety e liveness

SafetyLiveness (o progress)
Ideaqualcosa di male non accade maiqualcosa di buono prima o poi accade
DefinizioneP è vera in ogni stato di ogni computazione: è un invariantein ogni computazione esiste qualche stato in cui P è vera
Esempi dalle slidemutua esclusione: mai più di un processo nella regione critica; assenza di deadlock: nessun processo aspetta un evento che non può più avvenireassenza di starvation: un processo ottiene prima o poi la risorsa che chiede (CPU, lock); assenza di dormienza: un processo in attesa viene prima o poi risvegliato; comunicazione affidabile: un messaggio inviato viene ricevuto
Controesempiouno stato raggiungibile in cui P è falsauna computazione in cui P non diventa mai vera, di solito infinita: nel diagramma di stato, un ciclo

Le safety si esprimono di solito in negativo («non voglio mai Y») e si verificano stato per stato: per falsificarle basta uno stato raggiungibile. Per la liveness gli stati non bastano, perché P falsa ora può diventare vera dopo: bisogna esaminare scenari interi, con teoria e strumenti più complessi. Per esempio, se due processi incrementano atomicamente n partendo da 0, «n vale 2» non è una safety (all'inizio vale 0) ma una liveness: prima o poi, in ogni scenario, n diventa 2.

3. Fairness

La fairness è una proprietà di liveness per cui qualcosa di buono accade infinitamente spesso: ogni processo viene attivato infinite volte e ottiene il suo turno, e un'azione che può essere eseguita prima o poi lo è. È un requisito sullo scheduling: l'interleaving lo decide la politica di scheduling, quindi lo stesso programma può soddisfare o no una liveness a seconda della politica assunta. Un'azione atomica è incondizionata se non ha guardie (un assegnamento), condizionata se ne ha una (await B: può essere eseguita solo quando B è vera), eleggibile quando il processo è arrivato a eseguirla.

PoliticaGarantisce che prima o poi venga eseguita…
Fairness incondizionataogni azione incondizionata eleggibile
Fairness debolequanto sopra, più ogni azione condizionata eleggibile la cui condizione diventa vera e rimane vera
Fairness fortequanto sopra, più ogni azione condizionata eleggibile la cui condizione diventa vera infinitamente spesso

Ogni grado include il precedente, e debole e forte differiscono sulle condizioni intermittenti. Se un processo esegue await (x pari) mentre un altro incrementa x di continuo, la condizione diventa vera infinite volte ma non resta mai vera: la fairness debole non garantisce che l'attesa finisca, quella forte sì. Nessuna delle tre dice quando.

Scenari equi

Uno scenario è (incondizionatamente) equo se, in ogni suo stato, un'istruzione continuamente abilitata prima o poi compare nello scenario. L'esempio delle slide:

integer n := 0; boolean flag := false
p1: while flag = false          q1: flag := true
p2:   n := 1 - n

Termina sempre? Solo negli scenari equi. Lo scenario p1 p2 p1 p2 … non termina, ma non è equo: q1, un assegnamento sempre abilitato, non viene mai scelto. Gli scenari non equi servono però a modellare i guasti: in un sistema distribuito un processo la cui macchina cade non esegue più nulla, e se Q cade prima di q1, P non termina. Assumere la fairness vuol dire escludere questo caso.

4. La logica temporale lineare

Dalla logica proposizionale

Nel calcolo proposizionale una proprietà è una formula costruita da proposizioni atomiche con i connettivi ∧, ∨, ¬, → e ↔, vera o falsa in un singolo stato. Per i programmi concorrenti le proposizioni atomiche sono di due tipi:

Se p4 e q4 sono le sezioni critiche, p4 ∧ q4 descrive uno stato con entrambi in CS, e la mutua esclusione chiede che ¬(p4 ∧ q4) sia vera in ogni stato di ogni scenario. Ma una formula proposizionale parla di un solo stato: non sa dire «in ogni stato» né «prima o poi».

Le logiche temporali

Una logica temporale aggiunge operatori temporali alla logica proposizionale o dei predicati; per i programmi la introdusse Amir Pnueli (The Temporal Logic of Programs, 1977). Le famiglie sono due. La LTL (Linear Temporal Logic) vede il tempo come una sequenza lineare e discreta di stati: una formula si valuta su una computazione, e il programma la soddisfa se vale in tutti i suoi scenari, quindi basta uno scenario sbagliato per falsificarla. È la garanzia che serve per la correttezza. Le logiche ramificate, come la CTL (Computation Tree Logic), vedono da ogni stato un albero di futuri e sanno dire che una proprietà vale in alcuni o in tutti gli scenari che partono da lì.

Gli operatori

Siano s0, s1, … gli stati di una computazione. In uno stato si:

OperatoreSinonimiÈ vero in si se…
□ p (always, box)G p (globally)p è vera in ogni sj con j ≥ i
◇ p (eventually, diamond)F p (finally)p è vera in qualche sj con j ≥ i
○ p (next)X pp è vera in si+1
p U q (until)q è vera in qualche sj, j ≥ i, e p è vera in tutti gli sk con i ≤ k < j
p W q (weak until)come until, ma q può non arrivare mai: in quel caso p deve restare vera per sempre. «Finché q è falsa, p è vera»

L'inglese eventually non significa «eventualmente»: significa «prima o poi, di sicuro». Always serve per le safety, eventually per le liveness. Le proprietà di base delle slide:

LTL è anche un sistema deduttivo, con assiomi e regole di inferenza. Dalle slide: (◇□p ∧ ◇□q) → ◇□(p ∧ q) è un teorema, perché da un certo punto entrambe restano vere; (□◇p ∧ □◇q) → □◇(p ∧ q) è falsa, perché p e q possono alternarsi senza essere mai vere insieme.

La differenza tra until e weak until, valutata nello stato iniziale su tracce infinite scritte come «qualche stato, poi un ciclo per sempre» (calcolata con ltl-traces.cjs):

Traccia: valori di (p, q)p U qp W q
(0,1) per sempre: q vera subitoveravera
(1,0) poi (0,1) per sempre: p vale finché arriva qveravera
(1,0) per sempre: p sempre, q maifalsavera
(0,0) poi (0,1) per sempre: p è falsa prima che arrivi qfalsafalsa

5. Specificare le proprietà in LTL

Safety: □p, dove p = ¬q e q descrive lo stato da evitare. Nella versione abbreviata del problema della sezione critica, in cui le sezioni critiche sono p3 e q3, la mutua esclusione è

□ ¬(p3 ∧ q3)

Liveness: ◇p, dove p descrive la situazione desiderata. Se P aspetta di entrare in p2 e la sezione critica è p3, l'assenza di starvation si scrive in due passi:

Per l'esame

Ricci usa LTL in modo pratico: nel primo assignment, oltre all'architettura, si chiede di esprimere le proprietà di correttezza del proprio sistema. Saper scrivere una mutua esclusione con □ e una risposta con □(… → ◇ …), e spiegare perché, è il livello richiesto.

Oltre le slide

Anche la fairness di un'azione A si scrive in LTL. Debole: ◇□ abilitata(A) → □◇ eseguita(A). Forte: □◇ abilitata(A) → □◇ eseguita(A). Un'azione abilitata a intermittenza e mai eseguita, come await (x pari), soddisfa la prima e viola la seconda. Formule come queste sono ciò che un model checker verifica (capitolo 11).

6. Overtaking e k-bounded overtaking

L'assenza di starvation può essere una garanzia molto debole. Nello scenario

tryp, tryq, CSq, tryq, CSq, …, CSq, CSp      -- Q entra 1000 volte prima di P

P chiede di entrare, Q entra e riesce mille volte, e solo alla fine entra P. Non è starvation, perché ◇CSp vale, ma di solito si vuole che un processo entri entro un tempo ragionevole. Quando un processo entra al posto di un altro che aspettava si parla di overtaking (sorpasso).

La proprietà di k-bounded overtaking chiede che, da quando P tenta di entrare nella sezione critica, un altro processo possa entrarci al più k volte prima di P. Per esempio tryp, tryq, CSq, tryq, CSq, tryq, CSq, CSp rispetta il 3-overtaking. Si esprime con il weak until. Per k = 1:

tryp → (¬CSq) W (CSq W ((¬CSq) W CSp))

Da tryp la formula ammette un intervallo senza Q in CS, uno con Q in CS, un altro senza, che può chiudersi solo con P in CS: Q ha al più un intervallo continuo in sezione critica prima di P. Serve il weak until perché ogni intervallo può anche durare per sempre: con l'until la formula imporrebbe anche che P prima o poi entri, cioè conterrebbe una liveness.

Il k-bounded overtaking è una safety: vieta un sorpasso di troppo, ma non obbliga P a entrare. Va quindi affiancato alla liveness □(tryp → ◇CSp). Lo mostrano tre tracce, con la formula richiesta in ogni stato (□(tryp → …)):

Traccia1-bounded overtaking□(tryp → ◇CSp)
P chiede, Q entra una volta (per due passi), poi entra Pveravera
P chiede, Q entra due volte, poi entra Pfalsavera
P chiede e aspetta per sempre, nessuno entraverafalsa

Come si evita l'overtaking illimitato: con un protocollo di ingresso che limita l'attesa. Nell'algoritmo di Peterson ognuno cede il turno all'altro, quindi k = 1. Il biglietto del fornaio serve i processi in ordine di arrivo (capitolo 4). Un semaforo forte sveglia i processi in coda in ordine FIFO (capitolo 5). Un lock con test-and-set invece non dà alcun limite.

Verifica le tue conoscenze

Quando un programma concorrente è corretto?

Quando le sue proprietà di correttezza, di safety e di liveness, valgono in tutti gli scenari possibili, comprese le computazioni infinite. Non basta che valgano negli scenari osservati durante i test.

Che differenza c'è tra una proprietà di safety e una di liveness?

Una safety deve valere in ogni stato («non succede mai niente di male»), e la falsifica un singolo stato raggiungibile. Una liveness deve diventare vera in qualche stato di ogni computazione («prima o poi succede qualcosa di buono»), e la falsifica una computazione infinita in cui non accade mai.

L'assenza di deadlock è safety o liveness? E l'assenza di starvation?

L'assenza di deadlock è una safety: nessuno stato raggiungibile deve essere un deadlock. L'assenza di starvation è una liveness: ogni processo che chiede una risorsa prima o poi la ottiene.

Che cos'è la fairness e che cosa c'entra lo scheduling?

È la garanzia che un'azione che può essere eseguita prima o poi lo sia. È un requisito sulla politica di scheduling: a seconda di quale si assume, lo stesso programma soddisfa o no certe liveness.

Quali tipi di fairness esistono e come si distinguono?

Incondizionata: ogni azione incondizionata eleggibile viene eseguita. Debole: in più, ogni azione condizionata la cui condizione diventa e resta vera. Forte: in più, ogni azione condizionata la cui condizione diventa vera infinitamente spesso.

Perché serve una logica temporale e non basta quella proposizionale?

Una formula proposizionale è vera o falsa in un singolo stato. Le proprietà di correttezza parlano di come evolve il sistema («in ogni stato», «prima o poi»), e servono operatori temporali. LTL le valuta lungo le computazioni e richiede che valgano in tutti gli scenari.

Che differenza c'è tra p U q e p W q?

Entrambe chiedono che p valga finché non arriva q. L'until impone anche che q arrivi; il weak until no, e in quel caso p deve restare vera per sempre.

Come si scrivono in LTL mutua esclusione e assenza di starvation?

□¬(p3 ∧ q3) e □(p2 → ◇p3), dove p2 è il punto di attesa e p3 la sezione critica. Il □ davanti alla seconda la estende a ogni richiesta, non solo alla prima.

Che cos'è l'overtaking e come si limita?

È un processo che entra in sezione critica al posto di un altro che aspettava. Il k-bounded overtaking chiede che, da quando P chiede di entrare, gli altri entrino al più k volte prima di lui; si esprime con il weak until e lo garantiscono protocolli come Peterson (k = 1), il biglietto del fornaio e i semafori forti.

Da quali lezioni viene questo capitolo

Slide: module 1.2, seconda metà (correttezza, safety e liveness, fairness, verifica e testing, calcolo proposizionale, LTL, overtaking). Lezioni: 20 febbraio 2026 (safety, liveness e fairness, l'esempio con flag) · 27 febbraio (verifica, validazione e testing, heisenbug, LTL, until e weak until, overtaking) · 6 marzo (ripasso di safety e liveness in LTL). La mappa completa è in coverage-matrix.json.