CCS (Calculus of Communicating Systems) è un'algebra di processi ideata da Robin Milner per descrivere e ragionare su sistemi concorrenti. L'idea è quella di concentrarsi su pochissimi operatori primitivi, che catturano le caratteristiche essenziali della concorrenza, fornendo una sintassi concisa per costruire e comporre processi. Non si tratta di un linguaggio di programmazione completo, ma possiede comunque piena potenza computazionale (è Turing-equivalente).
Nei paradigmi sequenziali (si pensi a linguaggi come IMP o HOFL) valgono due proprietà comode ma limitanti:
- determinatezza: lo stesso programma, sullo stesso input, produce sempre lo stesso risultato
- due programmi che non terminano sono considerati equivalenti tra loro (entrambi denotano "bottom")
Nei paradigmi concorrenti la situazione cambia radicalmente:
- esiste un nondeterminismo intrinseco percepibile da un osservatore esterno
- la non terminazione può essere una caratteristica desiderabile (si pensi a un server, che deve restare sempre in ascolto)
- non tutti i processi non terminanti sono equivalenti tra loro
- l'interazione diventa una questione primaria
- servono dunque nuove nozioni di comportamento e di equivalenza
Per la comunicazione, CCS adotta uno scambio di messaggi binario (punto a punto) su canali. La semantica è data in stile SOS (Structural Operational Semantics) small-step, ovvero tramite un sistema di transizioni etichettate (Labelled Transition System, LTS): i processi sono gli stati, le interazioni in corso sono le etichette, e le transizioni sono definite tramite regole di inferenza per induzione sulla struttura dei processi.
Il cuore della semantica è la transizione etichettata
dove è un processo nel suo stato corrente, è l'interazione in corso con l'ambiente (con gli altri processi) e è lo stato del processo dopo l'interazione. Il numero di stati e transizioni può essere infinito.
Cosa è un processo ? Può essere un agente sequenziale, oppure un sistema in cui molti agenti sequenziali interagiscono.
Cosa è un'etichetta ? Può essere:
- un'azione (ad esempio un output), scritta ("invia sul canale ")
- una co-azione duale (ad esempio un input), scritta ("ricevi sul canale ")
- un'azione interna o silenziosa , che non comporta interazione con l'ambiente (rappresenta una comunicazione conclusa)
Possiamo essere ancora più astratti senza perdere espressività computazionale: trascuriamo i valori comunicati (come se ci fosse un canale dedicato per ciascun valore). Così:
- diventa semplicemente oppure semplicemente
- diventa semplicemente oppure semplicemente
- denota indifferentemente oppure
- denota la sua duale (assumendo )
Formalmente:
- è l'insieme delle azioni (input), su cui spazia
- è l'insieme delle co-azioni (output), su cui spazia , con
- è l'insieme delle azioni osservabili, su cui spaziano e
- è una distinta azione silenziosa
- è l'insieme di tutte le azioni, su cui spazia
La sintassi di CCS è definita dalla grammatica seguente (gli operatori sono elencati in ordine di precedenza):
dove:
- è il processo inattivo
- è una variabile di processo (usata per la ricorsione)
- è il prefisso d'azione
- è la restrizione di canale (canale ristretto)
- è la rietichettatura dei canali (channel relabelling)
- è la scelta nondeterministica (somma)
- è la composizione parallela
- è la ricorsione
Per la precedenza, ad esempio
va letto come
L'unico binder è l'operatore di ricorsione . La nozione di variabile (di processo) libera è definita come di consueto; un processo si dice chiuso se non ha variabili libere. Anche la nozione di sostituzione capture-avoiding è quella usuale, e i processi sono considerati a meno di alpha-rinomina delle variabili legate, ad esempio .
La semantica operazionale è data dalle seguenti regole di inferenza:
- Act (prefisso d'azione)
- Res (restrizione)
- Rel (rietichettatura)
- SumL e SumR (scelta)
- ParL e ParR (interleaving)
- Com (comunicazione/sincronizzazione)
- Rec (ricorsione)
L'LTS di CCS è di per sé infinito (uno stato per ciascun processo). L'LTS di un processo si ottiene considerando tutti gli stati raggiungibili a partire da : tale LTS può essere finito oppure infinito.
Esaminiamo ora gli operatori uno per uno.
Il processo inattivo non fa nulla: nessuna interazione con l'ambiente è possibile. Rappresenta un agente terminato e non ha alcuna regola di semantica operazionale associata.
Un processo prefissato da un'azione può compiere l'azione e proseguire come previsto; l'azione può comportare un'interazione con l'ambiente. Ad esempio
attende una moneta, poi eroga un caffè e infine si ferma:
Il processo può comportarsi o come o come . Ad esempio
attende una moneta, poi eroga un caffè oppure un tè, e infine si ferma.
La ricorsione si comporta come una definizione ricorsiva . Ad esempio
attende una moneta, poi eroga un caffè ed è di nuovo pronto, oppure eroga un tè e si ferma.
In alternativa, possiamo immaginare di avere a disposizione alcune costanti di processo insieme a un insieme di dichiarazioni della forma , una per ciascuna costante. La regola è
Ad esempio .
Un buffer di capacità 1 si può definire così:
Un buffer di capacità 2:
Un buffer booleano (che ricorda l'ultimo valore inserito):
I processi in esecuzione parallela possono interlacciare (interleave) le loro azioni, oppure sincronizzarsi quando vengono compiute azioni duali (regola Com, che produce un'azione silenziosa ). Ad esempio, con
abbiamo le tre possibilità:
È interessante mettere a confronto (due buffer di capacità 1 in parallelo) con il buffer di capacità 2: intuitivamente "simili", ma vedremo che non sono identici.
La restrizione rende il canale privato a : non è possibile alcuna interazione su con l'ambiente. Se è la composizione parallela di più processi, questi possono comunque sincronizzarsi su (tramite la regola Com, che produce e quindi non è bloccata dalla restrizione). Ad esempio:
Abbreviazioni: dato scriviamo invece di ; inoltre omettiamo i finali.
La rietichettatura rinomina i canali delle azioni secondo . Si assume e . Permette di riusare i processi. Ad esempio, con e , :
La combinazione di rietichettatura, parallelo e restrizione permette di collegare buffer in cascata: dati e , si definisce il collegamento
così che l'uscita del primo buffer comunichi (in modo privato, sul canale ) con l'ingresso del secondo.
La forma di ricorsione ammessa è molto generale e può produrre processi con infinite transizioni uscenti. Supponiamo e prendiamo (ovvero ): allora
Tali processi sono detti infinitely branching e sono indesiderabili (BAD).
I processi guarded garantiscono che le variabili di processo compaiano sotto un prefisso (la ricorsione è "protetta" da qualche azione). Sia un insieme di variabili di processo. Il predicato ("tutti i nomi definiti ricorsivamente sono guarded in , cioè se un nome di occorre libero in allora è prefissato da un'azione") è definito per induzione:
Un processo chiuso è guarded se è vero. Si noti la clausola chiave : passare sotto un prefisso "azzera" l'insieme dei nomi da proteggere, poiché essi risultano ormai guardati dall'azione .
Esempi:
- — unguarded
- — guarded
- — unguarded (la non è guardata)
- — unguarded
- — guarded
Valgono inoltre alcune proprietà (dimostrabili per induzione strutturale):
-
-
- le sostituzioni preservano la guardedness:
- le transizioni preservano la guardedness:
Il risultato fondamentale è che i processi guarded sono finitely branching:
Anche reintroducendo i valori, possiamo ricondurci al caso astratto. Le regole sono
Quando l'insieme dei valori è finito, , si ha
e un costrutto di tipo `receive ... end` con casi , e default corrisponde a .
Processi sintatticamente diversi possono esibire esattamente lo stesso comportamento. Vogliamo dunque una nozione di equivalenza che astragga dalla particolare sintassi. Ne esaminiamo tre, di "forza" decrescente.
Dati due LTS e , una biiezione è un isomorfismo se preserva e riflette le transizioni etichettate:
L'isomorfismo astrae dai nomi degli stati. Cattura ad esempio le equivalenze
Tuttavia l'isomorfismo è troppo stretto: e sono intuitivamente equivalenti (entrambi buffer di capacità 2) ma i loro LTS non sono isomorfi (il primo ha 3 stati, il secondo 4). Analogamente , e sono intuitivamente equivalenti ma non isomorfi.
In teoria degli automi corrisponde all'equivalenza di linguaggi. Una traccia (finita) è una sequenza di etichette etichettante un cammino
La semantica di tracce di un processo è
Due processi sono trace equivalent se hanno la stessa semantica di tracce: se e solo se . L'isomorfismo implica l'equivalenza di tracce, e la semantica di tracce è prefix-closed (se una traccia è presente, lo sono tutti i suoi prefissi).
L'equivalenza di tracce risolve i casi visti sopra (ad esempio , descrivibile dal vincolo su ogni prefisso). Tuttavia è troppo grossolana: identifica processi che intuitivamente vorremmo distinguere. Confrontiamo le due distributrici
Sono trace equivalenti, ma profondamente diverse dal punto di vista del cliente: nella prima inserisco la moneta e poi scelgo io la bevanda; nella seconda inserisco la moneta e la macchina sceglie per me. La differenza diventa drammatica componendo le macchine con un cliente che vuole solo caffè: con la seconda macchina si può raggiungere un deadlock.
Riassumendo:
- l'isomorfismo di grafi distingue troppi processi
- l'equivalenza di tracce identifica troppi processi
- serve una nozione di equivalenza intermedia: la bisimilarità (forte)
L'idea guida è: due processi sono equivalenti a meno che non si abbia una buona ragione per distinguerli.
La bisimulazione può essere descritta come un gioco tra due processi e due giocatori avversari:
- Alice, l'attaccante, vuole dimostrare che e non sono equivalenti
- Bob, il difensore, vuole dimostrare che e sono equivalenti
Il gioco è a turni; a ogni turno:
- Alice sceglie uno dei due processi e una sua transizione uscente
- Bob deve rispondere con una transizione dell'altro processo, con la stessa etichetta scelta da Alice
- al turno successivo, se c'è, si considera l'equivalenza dei processi di arrivo
Alice vince se a un certo punto riesce a fare una mossa che Bob non può eguagliare. Bob vince in tutti gli altri casi (se Alice non trova mosse, o se il gioco non termina). Alice ha una strategia vincente se riesce a confutare l'equivalenza di e in un numero finito di mosse.
Nell'esempio delle due distributrici, Alice gioca ; Bob può solo rispondere ; poi Alice gioca e Bob non può replicare (da non c'è una ): Alice vince.
La nozione di bisimulazione non è ristretta a CCS: si applica a qualsiasi LTS. Sia l'insieme dei processi e una relazione binaria (scriviamo quando ). è una bisimulazione forte se
Intuitivamente: se due processi sono in relazione, allora per ogni mossa di Alice, Bob può trovare una mossa che porta a processi ancora in relazione (cioè Bob ha una strategia vincente). Con "forte" si intende che tutte le transizioni, comprese quelle silenziose , devono essere eguagliate.
Esempi di bisimulazioni forti:
- è una bisimulazione forte
- l'identità è una bisimulazione forte
- ogni isomorfismo di grafi definisce una bisimulazione forte
A differenza degli isomorfismi, però, in una bisimulazione lo stesso processo può essere in relazione con molti processi. Ad esempio:
e, per i buffer,
Le bisimulazioni forti godono di buone proprietà di chiusura (le dimostrazioni sono tutte molto simili: si "segue" la mossa nella bisimulazione disponibile):
- Unione: se e sono bisimulazioni forti, allora lo è
- Inversa: se è una bisimulazione forte, allora lo è
- Composizione: se e sono bisimulazioni forti, allora lo è (la composizione si scrive talvolta anche )
La bisimilarità forte (spesso denotata in letteratura; usiamo per ricordare che è una congruenza) è la più grande bisimulazione forte:
Per dimostrare che due processi sono fortemente bisimili è dunque sufficiente esibire una bisimulazione forte che li contiene. Questa è la stessa relazione che la nota [[bisimulazione]] caratterizza come punto fisso del funzionale tramite il Teorema di Kleene.
Una singola bisimulazione forte non è necessariamente una relazione di equivalenza, ma la bisimilarità sì.
Teorema. La bisimilarità forte è una relazione di equivalenza ( per la riflessività; la simmetria segue dalla chiusura per inversa; la transitività dalla chiusura per composizione).
Teorema. La bisimilarità forte è essa stessa una bisimulazione forte (segue dalla chiusura per unione). Di conseguenza essa è la più grande bisimulazione forte, e ogni altra bisimulazione forte è contenuta in .
Teorema (definizione ricorsiva). soddisfa
(la direzione è immediata perché è una bisimulazione forte; la direzione si dimostra mostrando che è una bisimulazione forte).
Ogni relazione induce un'equivalenza , la più piccola equivalenza che contiene (chiusura riflessiva, simmetrica e transitiva). Se è una bisimulazione forte, allora anche lo è. Ogni equivalenza induce a sua volta una partizione in classi di equivalenza ; nella pratica, invece di elencare tutte le coppie di , conviene elencarne soltanto le classi.
Un'equivalenza è una congruenza quando è preservata da ogni contesto:
In tal caso possiamo sostituire processi equivalenti in qualsiasi contesto senza alterare la semantica astratta — proprietà essenziale per il ragionamento composizionale.
Teorema. La bisimilarità forte è una congruenza. In particolare, per ogni operatore:
1.
2.
3.
4.
5.
La tecnica di dimostrazione è sempre la stessa: si "indovina" una relazione abbastanza grande da contenere tutte le coppie di interesse, si mostra che è una bisimulazione forte, e dunque essa è contenuta in . Per i casi della scelta (4) e del parallelo si deve aggiungere alla relazione candidata, poiché i processi di arrivo possono essere già bisimili senza essere strutturalmente della forma prevista.
Tutte le leggi seguenti valgono a meno di bisimilarità forte e si dimostrano esibendo una bisimulazione forte opportuna.
Per scelta e parallelo (monoidi commutativi, con anche idempotente):
Per la restrizione:
Per la rietichettatura: