Skip to main content

Process Algebra

Imagine you are building a system for a vending machine for a train station that prints 2 different kinds of tickets: Children tickets, which are only 5€ and Adult tickets, which cost 10€. The vending machine takes only 5€ and 10€ bills. So if you want an adult ticket, you either need to put in 2x5€ or 1x10€.

We can simply sketch this process like this:

sketch of the system

Now, we can actually model this using algebra. This is what we'll be talking about today.

"But why is this useful? I will probably never design Vending machine systems"

This is actually really important for modeling distributed and concurrent systems. Especially also for AI engineering this could be very important. Since we begin to add more and more LLM Agents into our systems, we need to make our systems concurrent. Learning the Algebraic basics of these systems helps not only in designing them, but also to find alternatives, compare different versions and prove if they will work or fail.

Rules

Syntax

Now let's get back to the vending machine example. We can rewrite this system as such:

  • Entry=5Euro.Paid5+10Euro.Paid10\text{Entry} = \text{5Euro.Paid5} + \text{10Euro.Paid10}
  • Paid5=button.childTicket.Print+5Euro.Paid10\text{Paid5} = \text{button.childTicket.Print} + \text{5Euro.Paid10}
  • Paid10=button.adultTicket.Print\text{Paid10} = \text{button.adultTicket.Print}

Now this looks a bit weird. So let's break it down.

  • States: Entry, Paid5 and Paid10 refer to states. you can either be in the beginning, have paid 5 Euros or have paid 10 Euros
  • ==: The Equals operator show Processes are possible from a state. e.g. If you're in the Paid10 state, all you can do is print the ticket
  • ++: The + Operator just shows that these processes are concurrent. They are not dependent to each other.
  • ..: The dot operator (.) represents a sequence of actions. For example, in 5Euro.Paid5, inserting a 5€ bill leads to the Paid5 state.

With that said, let's redraw our diagram.

new-diagram

Operational Semantcs

Operational semantics gives you rules that say “if a process looks like this, then it can do that action and turn into that other process.” You can think of it as the little engine that tells you how your algebraic spec runs, step by step.

They look like this:

operational-semantics example

a.P means “do action a first, then behave like process P.”

The horizontal line with nothing above it means “this rule always applies”, there are no extra conditions.

Below the line is the conclusion: a.PaPa.P \xrightarrow{a} P reads “Process a.P can perform action a and then become P.”

a and P are called Meta variables. Where a is an action and P is a process.

Parameters

You can also add parameters to both actions and equations, just like functions. For our previous example, it could look like this:

  • P=euro(x).Paid(x)P = euro(x).Paid(x)
  • Paid(5)=button.print(childTicket).P+euro(5).Paid(10)Paid(5) = button.print(childTicket).P + euro(5).Paid(10)
  • Paid(10)=button.print(adultTicket).PPaid(10) = button.print(adultTicket).P

This turns Process Algebra actually to a real programming language. (Which sounds super weird).

Edit: There are actually some languages based on Process Algebra, such as occam-pi. Never heard of it before but would be cool looking into in the future

Parallell processes

Process Algebra defines 2 parallell processes as such:

You take two processes, P and Q, and run them “side-by-side." -> They synchronize (must act together) on any action in the set Θ. -> They interleave independently on actions not in Θ.

But what is Θ? It's the Intersection between the sum of P with the sum of Q:

Θ  =  Σ(P)    Σ(Q)\Theta \;=\; \Sigma(P)\;\cap\;\Sigma(Q)

Example:

  • If P can do printChild,giveChange\mathsf{printChild},\mathsf{giveChange}
  • and Q can do printChild,log\mathsf{printChild},\mathsf{log},
  • then Θ\Theta = printChild\mathsf{printChild}

So if we define RR_{\parallel} as such:

PaPQaQaΘP  Θ  Q  a  P  Θ  Q(R)\frac{P \xrightarrow{a} P{\prime} \quad Q \xrightarrow{a} Q{\prime} \quad a \in \Theta} {P \;\parallel_{\Theta}\; Q \;\xrightarrow{a}\; P{\prime} \;\parallel_{\Theta}\; Q{\prime}} \quad (R_{\parallel})

We would read it like this:

  1. Above the line we list the premises—what must be true first:
  • PaPP \xrightarrow{a} P{\prime} means “P can do action a and become PP{\prime}.”
  • QaQQ \xrightarrow{a} Q{\prime} means “Q can also do a and become QQ{\prime}.”
  • aΘa \in \Theta says this action is one of the shared ones.
  1. Below the line is the conclusion:
PΘQ  a  PΘQP \parallel_{\Theta} Q \;\xrightarrow{a}\; P{\prime} \parallel_{\Theta} Q{\prime}

i.e. “When running in parallel, they must fire a together, and end up in the pair of successor states.”

"But what about actions that are not shared"?

If only one side can do an action b∉Θb \not\in \Theta, it may do it “solo” and the other side just idles:

PbPbΘPΘQ  b  PΘQQbQbΘPΘQ  b  PΘQ\frac{P \xrightarrow{b} P{\prime} \quad b \notin \Theta} {P \parallel_{\Theta} Q \;\xrightarrow{b}\; P{\prime} \parallel_{\Theta} Q} \quad \frac{Q \xrightarrow{b} Q{\prime} \quad b \notin \Theta} {P \parallel_{\Theta} Q \;\xrightarrow{b}\; P \parallel_{\Theta} Q{\prime}}

Web servers, database writes, AI-agent threads all run in parallel. That's why it's important to understand these systems in a formal setting.

Linking

linking (often called renaming) lets you take a “template” process and “plug in” different action names—just like you’d instantiate a function or template in code. Concretely.

E.g. If we have this: P=abcPP = a → b → c → P

we can produce two fresh copies that behave the same except they use different labels:

  • P[x/b]P[x/b] replaces every occurrence of bb in PP with xx.
  • P[y/b]P[y/b] replaces every bb with yy.

But why rename/link?

2 Reasons really:

  1. Instantiate templates: Just like we just saw, you can write a generic process and then just make new ones based on the template
  2. Connect subsystems: Suppose you have a payment handler that signals paid when money arrives, and a printer that listens for paid. You can link them by renaming as such:
Pay=insertCoinpaidSTOPPay = insertCoin → paid → STOP Print=paidprintTicketSTOPPrint = paid → printTicket → STOP System=Pay[insertCoin/payment]{payment}PrintSystem = Pay[insertCoin/payment] ‖\{payment\}‖ Print

Here Pay[insertCoin/payment]Pay[insertCoin/payment] makes its internal insertCoin appear as payment so it synchronizes with Print.

Renaming Ruls

(R[])PaQP[b/a]bQ[b/a]\quad(R_{[]}) \frac{P \xrightarrow{a} Q} {P[b/a] \xrightarrow{b} Q[b/a]}
  • Above the line: if in the original process P you can do action a and go to Q,
  • Below the line: then in the renamed process P[b/a]P[b/a] you can do b and go to the renamed successor Q[b/a]Q[b/a].

Milner's Scheduler

This is a classical example.

You have nn worker processes P1,P2,,PnP_1, P_2, \dots, P_n. Each worker:

  1. starts its job (aia_i),
  2. does some hidden work (ziz_i),
  3. finishes its job (bib_i),
  4. then loops back to wait for the next start.

You want a round-robin policy: first P1P_1 runs, then P2P_2, …, then PnP_n, then back to P1P_1, and so on—no skipping, no out of order.

First, ignore the subscripts and write a template process PP:

P=azbPP = a \rightarrow z \rightarrow b \rightarrow P

  • aa = "start the job"
  • zz = "do internal work" (invisible to the scheduler)
  • bb = "finish the job"
  • then repeat

We need nn copies, each with its own labels:

  • For worker ii, rename:
    • aaia \mapsto a_i,
    • zziz \mapsto z_i,
    • bbib \mapsto b_i.

Formally: Pi  =  P[ai/a,  zi/z,  bi/b].P_i \;=\; P[a_i/a,\; z_i/z,\; b_i/b].

If you just ran R=P1    P2      PnR = P_1 \;\parallel\; P_2 \;\parallel\;\dots\parallel\; P_n then all PiP_i would be free to start, finish, or interleave in any order—which doesn't enforce the strict 12n1 \rightarrow 2 \rightarrow \dots \rightarrow n sequence.

To force the order, we wrap each PiP_i in a small controller QiQ_i. The job of QiQ_i is:

  1. ask PiP_i to start (aia_i),
  2. once aia_i fires, pass a token xix_i to the next controller Qi+1Q_{i+1},
  3. wait for PiP_i to finish (bib_i),
  4. then wait to receive the token xi1x_{i-1} from the previous controller,
  5. loop.

In symbols, the generic proxy QQ (before plugging in names) has this shape:

Q=a.x.b.y.QQ = a . x . b . y . Q

  • After doing aa, it sends xx.
  • After doing bb, it waits for yy.
  • Then repeats.

Now we make nn copies Q1,,QnQ_1,\dots,Q_n by renaming:

  • Q1=Q[a1/a,  x1/x,  b1/b,  xn/y]Q_1 = Q[a_1/a,\; x_1/x,\; b_1/b,\; x_n/y]
  • For 2in2 \leq i \leq n: Qi=Q[ai/a,  xi/x,  bi/b,  xi1/y].Q_i = Q[a_i/a,\; x_i/x,\; b_i/b,\; x_{i-1}/y].

Then we compose all QiQ_i in parallel: R=Q1    Q2      Qn.R = Q_1 \;\parallel\; Q_2 \;\parallel\;\dots\parallel\; Q_n.

Because each QiQ_i:

  • must synchronize on xix_i with Qi+1Q_{i+1},
  • must synchronize on yiy_i (which is really xi1x_{i-1}) with Qi1Q_{i-1},

the only legal order of events is:

  1. a1a_1, z1z_1, b1b_1 (run job 1),
  2. then a2a_2, z2z_2, b2b_2,
  3. …,
  4. ana_n, znz_n, bnb_n,
  5. then back to a1a_1, etc.

An initial attempt used Q=a.x.b.y.QQ = a.x.b.y.Q which forces "start → send token → finish → receive token." But that can deadlock if a finish (bb) and the next start (aa) collide.

The fixed version instead writes Q=a.x.(b.y  +  y.b)  .QQ = a.x.(\,b.y\;+\;y.b\,)\;.Q so after sending its token it can either:

  • do bb then wait yy, or
  • wait yy then do bb,

whichever comes first. This decoupling lets the "finish" of one job and the "start" of the next happen in any safe order without deadlock.