Ritorna all'inizio


CCS

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

Sequenziale vs concorrente

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.

Transizioni etichettate

Il cuore della semantica è la transizione etichettata

pμq

dove p è un processo nel suo stato corrente, μ è l'interazione in corso con l'ambiente (con gli altri processi) e q è lo stato del processo dopo l'interazione. Il numero di stati e transizioni può essere infinito.

Stati ed etichette

Cosa è un processo p? 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 α!v ("invia v sul canale α")

- una co-azione duale (ad esempio un input), scritta α?v ("ricevi v 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ì:

- α!v diventa semplicemente αv¯ oppure semplicemente α¯

- α?v diventa semplicemente αv oppure semplicemente α

- λ denota indifferentemente α oppure α¯

- λ¯ denota la sua duale (assumendo α¯¯=α)

Le etichette di CCS

Formalmente:

- C è l'insieme delle azioni (input), su cui spazia α

- C¯ è l'insieme delle co-azioni (output), su cui spazia α¯, con CC¯=

- Λ=CC¯ è l'insieme delle azioni osservabili, su cui spaziano λ e λ¯

- τΛ è una distinta azione silenziosa

- L=Λ{τ} è l'insieme di tutte le azioni, su cui spazia μ

Sintassi

La sintassi di CCS è definita dalla grammatica seguente (gli operatori sono elencati in ordine di precedenza):

p,q::=nilxμ.pp\αp[ϕ]p+qpqrecx.p

dove:

- nil è il processo inattivo

- x è una variabile di processo (usata per la ricorsione)

- μ.p è il prefisso d'azione

- p\α è la restrizione di canale (canale ristretto)

- p[ϕ] è la rietichettatura dei canali (channel relabelling)

- p+q è la scelta nondeterministica (somma)

- pq è la composizione parallela

- recx.p è la ricorsione

Per la precedenza, ad esempio

recx.coffee.x+tea.nilwater.nil

va letto come

recx.(((coffee.x)+tea.nil)water.nil)

L'unico binder è l'operatore di ricorsione recx.p. La nozione di variabile (di processo) libera fv(p) è definita come di consueto; un processo si dice chiuso se non ha variabili libere. Anche la nozione di sostituzione capture-avoiding p[q/x] è quella usuale, e i processi sono considerati a meno di alpha-rinomina delle variabili legate, ad esempio recx.coin.x=recy.coin.y.

Semantica operazionale

La semantica operazionale è data dalle seguenti regole di inferenza:

- Act (prefisso d'azione)

μ.pμp

- Res (restrizione)

pμqμ{α,α¯}p\αμq\α

- Rel (rietichettatura)

pμqp[ϕ]ϕ(μ)q[ϕ]

- SumL e SumR (scelta)

p1μqp1+p2μqp2μqp1+p2μq

- ParL e ParR (interleaving)

p1μq1p1p2μq1p2p2μq2p1p2μp1q2

- Com (comunicazione/sincronizzazione)

p1λq1p2λ¯q2p1p2τq1q2

- Rec (ricorsione)

p[recx.p/x]μqrecx.pμq

L'LTS di CCS è di per sé infinito (uno stato per ciascun processo). L'LTS di un processo p si ottiene considerando tutti gli stati raggiungibili a partire da p: tale LTS può essere finito oppure infinito.

Esaminiamo ora gli operatori uno per uno.

Processo Nil

nil

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.

Prefisso d'azione

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

coin.coffee¯.nil

attende una moneta, poi eroga un caffè e infine si ferma:

coin.coffee¯.nilcoincoffee¯.nilcoffee¯nil

Scelta nondeterministica

Il processo p1+p2 può comportarsi o come p1 o come p2. Ad esempio

coin.(coffee¯.nil+tea¯.nil)

attende una moneta, poi eroga un caffè oppure un tè, e infine si ferma.

Ricorsione

La ricorsione si comporta come una definizione ricorsiva letx=pinx. Ad esempio

recx.coin.(coffee¯.x+tea¯.nil)

attende una moneta, poi eroga un caffè ed è di nuovo pronto, oppure eroga un tè e si ferma.

Ricorsione tramite costanti di processo

In alternativa, possiamo immaginare di avere a disposizione alcune costanti di processo A insieme a un insieme Δ di dichiarazioni della forma Ap, una per ciascuna costante. La regola è

ApΔpμqAμq

Ad esempio Pcoin.(coffee¯.P+tea¯.nil).

Un buffer di capacità 1 si può definire così:

Δ={B01in.B11,B11out¯.B01}ovverorecx.in.out¯.x

Un buffer di capacità 2:

B02in.B12B12in.B22+out¯.B02B22out¯.B12

Un buffer booleano (che ricorda l'ultimo valore inserito):

Bint.Bt+inf.BfBtoutt¯.BBfoutf¯.B

Composizione parallela

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

Pcoin¯.coffee.nilMcoin.(coffee¯.nil+tea¯.nil)

abbiamo le tre possibilità:

PMcoin¯coffee.nilM

PMcoinP(coffee¯.nil+tea¯.nil)

PMτcoffee.nil(coffee¯.nil+tea¯.nil)

È interessante mettere a confronto B01B01 (due buffer di capacità 1 in parallelo) con il buffer B02 di capacità 2: intuitivamente "simili", ma vedremo che non sono identici.

Restrizione

pμqμ{α,α¯}p\αμq\α

La restrizione rende il canale α privato a p: non è possibile alcuna interazione su α con l'ambiente. Se p è 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:

(PM)\coin\coffee\teaτ(coffee.nilcoffee¯.nil+tea¯.nil)\coin\coffee\teaτ(nilnil)\coin\coffee\tea

Abbreviazioni: dato S={α1,,αn} scriviamo p\S invece di p\α1\αn; inoltre omettiamo i nil finali.

Rietichettatura

pμqp[ϕ]ϕ(μ)q[ϕ]

La rietichettatura rinomina i canali delle azioni secondo ϕ. Si assume ϕ(τ)=τ e ϕ(λ¯)=ϕ(λ)¯. Permette di riusare i processi. Ad esempio, con Pcoin¯.coffee e ϕ(coin)=moneta, ϕ(coffee)=caffè:

P[ϕ]moneta¯caffè[ϕ]caffènil[ϕ]

La combinazione di rietichettatura, parallelo e restrizione permette di collegare buffer in cascata: dati η(out)=c e ϕ(in)=c, si definisce il collegamento

pq(p[η]q[ϕ])\c

così che l'uscita del primo buffer comunichi (in modo privato, sul canale c) con l'ingresso del secondo.

Processi guarded

La forma di ricorsione ammessa è molto generale e può produrre processi con infinite transizioni uscenti. Supponiamo pμq e prendiamo Precx.px (ovvero PpP): allora

PμqPPμpqPPμppqP

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 X un insieme di variabili di processo. Il predicato G(p,X) ("tutti i nomi definiti ricorsivamente sono guarded in p, cioè se un nome di X occorre libero in p allora è prefissato da un'azione") è definito per induzione:

G(nil,X)trueG(p[ϕ],X)G(p,X)

G(x,X)xXG(p+q,X)G(p,X)G(q,X)

G(μ.p,X)G(p,)G(pq,X)G(p,X)G(q,X)

G(p\α,X)G(p,X)G(recx.p,X)G(p,X{x})

Un processo chiuso p è guarded se G(p,) è vero. Si noti la clausola chiave G(μ.p,X)G(p,): passare sotto un prefisso "azzera" l'insieme dei nomi da proteggere, poiché essi risultano ormai guardati dall'azione μ.

Esempi:

- recx.xunguarded

- recx.α.recy.xguarded

- recx.α.recy.x+yunguarded (la y non è guardata)

- recx.α.recy.xyunguarded

- recx.α.recy.xβ.yguarded

Valgono inoltre alcune proprietà (dimostrabili per induzione strutturale):

- G(p,X{x})G(p,X)

- G(p,X)xfv(p)G(p,X{x})

- le sostituzioni preservano la guardedness: G(p,X)i[1,n]G(pi,X)G(p[p1/x1,,pn/xn],X)

- le transizioni preservano la guardedness: G(p,X)pμqG(q,)

Il risultato fondamentale è che i processi guarded sono finitely branching:

G(p,)μ.{qpμq} è un insieme finito

CCS con passaggio di valori

Anche reintroducendo i valori, possiamo ricondurci al caso astratto. Le regole sono

α!v.pαv¯pα?x.pαvp[v/x]

Quando l'insieme dei valori è finito, V{v1,,vn}, si ha

α!v.pαv¯.pα?x.pαv1.p[v1/x]++αvn.p[vn/x]

e un costrutto di tipo `receive ... end` con casi vp, wq e default \_r corrisponde a αv.p+αw.q+zv,wαz.r.

Semantica astratta

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.

Isomorfismo di grafi

Dati due LTS G=(V,E) e G=(V,E), una biiezione f:VV è un isomorfismo se preserva e riflette le transizioni etichettate:

vμwf(v)μf(w)

L'isomorfismo astrae dai nomi degli stati. Cattura ad esempio le equivalenze

nilnil\αnil[ϕ]pp+nilp+ppnilp+qq+ppqqp

Tuttavia l'isomorfismo è troppo stretto: B02 e B01B01 sono intuitivamente equivalenti (entrambi buffer di capacità 2) ma i loro LTS non sono isomorfi (il primo ha 3 stati, il secondo 4). Analogamente recx.α.x, recx.α.α.x e α.recx.α.x sono intuitivamente equivalenti ma non isomorfi.

Equivalenza di tracce

In teoria degli automi corrisponde all'equivalenza di linguaggi. Una traccia (finita) è una sequenza di etichette etichettante un cammino

pμ1μ2μkqovverop=p0μ1p1μ2μkpk=q

La semantica di tracce di un processo è

T(p)={μ1μ2μkq.pμ1μ2μkq}

Due processi sono trace equivalent se hanno la stessa semantica di tracce: ptrq se e solo se T(p)=T(q). 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 T(B02)=T(B01B01), descrivibile dal vincolo 0#in-#out2 su ogni prefisso). Tuttavia è troppo grossolana: identifica processi che intuitivamente vorremmo distinguere. Confrontiamo le due distributrici

coin.(coffee¯+tea¯)ecoin.coffee¯+coin.tea¯

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 Ccoin¯.coffee.C che vuole solo caffè: con la seconda macchina si può raggiungere un deadlock.

Verso la bisimilarità

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.

Il gioco della bisimulazione

La bisimulazione può essere descritta come un gioco tra due processi p,q e due giocatori avversari:

- Alice, l'attaccante, vuole dimostrare che p e q non sono equivalenti

- Bob, il difensore, vuole dimostrare che p e q 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 p e q in un numero finito di mosse.

Nell'esempio delle due distributrici, Alice gioca coin.coffee¯+coin.tea¯coincoffee¯; Bob può solo rispondere coin.(coffee¯+tea¯)coincoffee¯+tea¯; poi Alice gioca coffee¯+tea¯tea¯nil e Bob non può replicare (da coffee¯ non c'è una tea¯): Alice vince.

Bisimulazione forte

La nozione di bisimulazione non è ristretta a CCS: si applica a qualsiasi LTS. Sia P l'insieme dei processi e RP×P una relazione binaria (scriviamo pRq quando (p,q)R). R è una bisimulazione forte se

p,q.(p,q)R{μ,p.pμpq.qμqpRqμ,q.qμqp.pμppRq

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à Id{(p,p)pP} è una bisimulazione forte

- ogni isomorfismo di grafi definisce una bisimulazione forte Rf{(p,f(p))}

A differenza degli isomorfismi, però, in una bisimulazione lo stesso processo può essere in relazione con molti processi. Ad esempio:

R{(recx.α.x,recx.α.α.x),(recx.α.x,α.recx.α.α.x)}

e, per i buffer,

R{(B02,B01B01),(B12,B11B01),(B12,B01B11),(B22,B11B11)}

Proprietà di chiusura

Le bisimulazioni forti godono di buone proprietà di chiusura (le dimostrazioni sono tutte molto simili: si "segue" la mossa nella bisimulazione disponibile):

- Unione: se R1 e R2 sono bisimulazioni forti, allora R1R2 lo è

- Inversa: se R è una bisimulazione forte, allora R-1{(q,p)pRq} lo è

- Composizione: se R1 e R2 sono bisimulazioni forti, allora R2R1{(p,q)r.pR1rrR2q} lo è (la composizione si scrive talvolta anche R1R2)

Bisimilarità forte

La bisimilarità forte (spesso denotata in letteratura; usiamo per ricordare che è una congruenza) è la più grande bisimulazione forte:

pqsseR bisimulazione forte con (p,q)RovveroRs.b.R

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 (Id 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

p,q.pq{μ,p.pμpq.qμqpqμ,q.qμqp.pμppq

(la direzione è immediata perché è una bisimulazione forte; la direzione si dimostra mostrando che R{(p,q)} è una bisimulazione forte).

Relazione ed equivalenza indotta

Ogni relazione R induce un'equivalenza R, la più piccola equivalenza che contiene R (chiusura riflessiva, simmetrica e transitiva). Se R è una bisimulazione forte, allora anche R lo è. Ogni equivalenza induce a sua volta una partizione in classi di equivalenza [p]={qpq}; nella pratica, invece di elencare tutte le coppie di R, conviene elencarne soltanto le classi.

Compositionalità

Un'equivalenza è una congruenza quando è preservata da ogni contesto:

[].p,q.pq[p][q]

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. pqμ.pμ.q

2. pqp\αq\α

3. pqp[ϕ]q[ϕ]

4. p0q0p1q1p0+p1q0+q1

5. p0q0p1q1p0p1q0q1

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.

CCS: alcune leggi

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

p+nilppnilp

p+qq+ppqqp

p+(q+r)(p+q)+rp(qr)(pq)r

p+pp

Per la restrizione:

nil\αnil

(μ.p)\αnilse μ{α,α¯}

(μ.p)\αμ.(p\α)se μ{α,α¯}

(p+q)\α(p\α)+(q\α)

p\α\αp\αp\α\βp\β\α

Per la rietichettatura:

nil[ϕ]nil

(μ.p)[ϕ]ϕ(μ).(p[ϕ])

(p+q)[ϕ](p[ϕ])+(q[ϕ])

p[ϕ][η]p[ηϕ]

Articoli correlati