Algorithm Search⌘K

Proofs: Disruptor ring buffer

These are the human proofs of Disruptor ring buffer, at full length.

The Disruptor ring buffer is a linearizable bounded broadcast channel full

This proof shows: correctness, linearizability, progress.

We use the pseudocode of the page, with ring size NN. Every read and write of one sequence number, and the fetch-and-add of line L1, is atomic, and reads see the last write (sequential consistency of the shared words). Each consumer cc is one thread, so its calls of CONSUME do not overlap; it is the only writer of c.seqc.seq. The dependency graph of the consumers has no cycle. Let xsx_s be the item of the producer that got ss at line L1.

Lemma 1 (monotone sequences). R.claimR.claim, R.cursorR.cursor and every c.seqc.seq never decrease. The cursor takes the values −1,0,1,2,…-1, 0, 1, 2, \ldots in this order, and R.cursor≥sR.cursor \ge s holds exactly from the moment the producer of ss runs line L5.

Proof. Line L1 only adds. A producer with number ss sets the cursor to ss at line L5 only after it has read the value s−1s - 1 at line L4, and no other producer can write the cursor in between: a write of s′s' needs the value s′−1s' - 1, the numbers are distinct, and the only producer that waits for the value s−1s - 1 is the one with ss. So each write raises the cursor by 1. Consumer cc sets c.seqc.seq at line L9 to a≥next=c.seq+1a \ge next = c.seq + 1, read at line L6, and nobody else writes c.seqc.seq between lines L6 and L9. □\square

Lemma 2 (reads of a minimum). If a thread reads several monotone words one after another and computes the minimum mm of the values, then mm is at most the true minimum of the words at the time of the last read.

Proof. Each value read is at most the current value of its word at every later time, so the minimum of the values is at most the minimum of the current values at the time of the last read. □\square

Lemma 3 (invariant). At all times, for each consumer cc and each d∈c.depsd \in c.deps, c.seq≤d.seq≤R.cursorc.seq \le d.seq \le R.cursor. Also, a producer passes line L2 with number ss only when s−N≤c.seqs - N \le c.seq for all consumers cc.

Proof. The value aa written at line L9 is at most the cursor and each d.seqd.seq at the time of the reads of line L7 (Lemma 2 for the minimum), and by Lemma 1 these values only grow until line L9. So the inequalities hold after each write of c.seqc.seq, and writes of other words only make them easier. The last claim is the condition of line L2 together with Lemma 2. □\square

Lemma 4 (slots). If a consumer cc reads slot s mod Ns \bmod N at line L8 in a call with next≤s≤anext \le s \le a, it reads xsx_s.

Proof. At line L7, cc read a value at least a≥sa \ge s from the cursor, so by Lemma 1 the producer of ss has run line L5, and hence line L3 before it: the slot got xsx_s. The only later writes to this slot are by producers of s+kNs + kN with k≥1k \ge 1 at line L3, after they passed line L2. By Lemma 3, that needs s+kN−N≥ss + kN - N \ge s to be at most c.seqc.seq at some time, so c.seq≥sc.seq \ge s. But during this call c.seq=next−1<sc.seq = next - 1 < s until line L9, which comes after line L8. So the slot still holds xsx_s when cc reads it. □\square

The abstraction. At each moment, let PP be the list x0,…,xR.cursorx_0, \ldots, x_{R.cursor}, and posc=c.seq+1pos_c = c.seq + 1. Initially PP is empty and every posc=0pos_c = 0. Only line L5 changes PP, and only line L9 changes poscpos_c.

Theorem (linearizability). Each history of the ring buffer is linearizable with respect to the sequential broadcast channel of the page.

Proof. Linearize each publish that has run line L5 at that step, and each consume that has run line L9 at that step; remove other pending calls. Each point lies inside its call, so the order of the points extends real-time order. We show that, at each point, the abstract operation is allowed and gives the same change and result.

A publish with number ss at line L5. By Lemma 1, before this step R.cursor=s−1R.cursor = s - 1, so ∣P∣=s|P| = s, and the step appends xsx_s, the abstract publish, with result s=∣P∣−1s = |P| - 1 after the append. The abstract precondition ∣P∣−min⁡cposc<N|P| - \min_c pos_c < N, that is s−N≤min⁡cc.seqs - N \le \min_c c.seq, held when the producer passed line L2 (Lemma 3), and it still holds at line L5 because every c.seqc.seq only grows.

A consume of cc at line L9, with nextnext and aa as in the code. Then posc=nextpos_c = next before the step, and a≥nexta \ge next. At line L7, aa was at most the cursor and every d.seqd.seq for d∈c.depsd \in c.deps (Lemma 2), and these only grow, so at line L9 a+1≤∣P∣a + 1 \le |P| and a+1≤posda + 1 \le pos_d. So the abstract consume may choose the end a+1a + 1, and it sets poscpos_c to a+1a + 1, as line L9 does. By Lemma 4 the batch returned is xnext,…,xax_{next}, \ldots, x_a, which is P[posc..a]P[pos_c .. a], the abstract result. □\square

Theorem (progress). No thread holds a lock. A thread waits only at line L2, line L4 or line L7, and:

  1. A consume of cc at line L7 passes within O(∣c.deps∣)O(|c.deps|) of its own steps after a moment from which the abstract consume of cc is allowed, that is, min⁡(∣P∣,min⁡d∈c.depsposd)>posc\min(|P|, \min_{d \in c.deps} pos_d) > pos_c.
  2. A publish at line L2 passes within O(k)O(k) of its own steps, for kk consumers, after a moment from which the gate condition s−N≤min⁡cc.seqs - N \le \min_c c.seq holds; with one producer this is exactly the abstract condition that the ring is not full.
  3. A publish with number ss waits at line L4 only until the producers of all smaller numbers have run line L5. With one producer, it never waits at line L4.
  4. So with one producer, every call finishes within O(k+b)O(k + b) of its own steps, where bb is the batch size, once its abstract operation is allowed: the structure is wait-free apart from the waits that the abstract type itself demands.

Proof. Part 1: from that moment on, each reading of line L7 gives a minimum of at least the values at its first read, which by Lemma 1 are at least those at the moment, so the test passes on the next round, after O(∣c.deps∣)O(|c.deps|) steps. Lines L8 to L10 take O(b)O(b) steps. Part 2 is the same argument for line L2. With one producer, R.claim=∣P∣R.claim = |P| while the producer is outside a call, so s=∣P∣s = |P| and the gate condition is the abstract condition. Part 3 is Lemma 1: the cursor reaches s−1s - 1 when the producer of s−1s - 1 runs line L5, and by induction this needs the producers of all smaller numbers. With one producer, all smaller numbers were published by its earlier calls. Part 4 combines parts 1 to 3.

With many producers the structure is blocking: a producer that stops between lines L1 and L5 stops all producers with larger numbers at line L4, and, once the ring fills, all consumers at line L7. If no thread stops and every consumer keeps calling CONSUME, every publish finishes. Take the smallest pending number ss. All smaller numbers are published, so R.cursor=s−1R.cursor = s - 1 and the producer of ss waits only at line L2. Go through the consumers in an order in which each comes after its dependencies, which exists since the graph has no cycle. A consumer with no dependencies sees the cursor at s−1s - 1 and, by part 1, reaches c.seq=s−1c.seq = s - 1 within its next calls; a consumer whose dependencies have all reached s−1s - 1 does the same. So every c.seqc.seq reaches s−1≥s−Ns - 1 \ge s - N, the gate opens, and by part 2 and part 3 the publish of ss finishes. By induction on ss every publish finishes. ■\blacksquare

Back to Disruptor ring buffer