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:

Nei paradigmi concorrenti la situazione cambia radicalmente:

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:

Possiamo essere ancora più astratti senza perdere espressività computazionale: trascuriamo i valori comunicati (come se ci fosse un canale dedicato per ciascun valore). Così:

Le etichette di CCS

Formalmente:

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:

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:

μ.pμp

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

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

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

p1μq1p1p2μq1p2p2μq2p1p2μp1q2

p1λq1p2λ¯q2p1p2τq1q2

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:

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

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

Il gioco è a turni; a ogni turno:

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:

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

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