Algorithm Search⌘K

Proofs: LCRQ (linked concurrent ring queue)

These are the human proofs of LCRQ (linked concurrent ring queue), at full length.

The LCRQ is a linearizable, lock-free FIFO queue full

This proof shows: correctness, linearizability, progress.

We use the pseudocode of the page. Reads, the fetch-and-add and the closing of a tail word, the fetch-and-add of a head word, the CAS2 of a cell and the CAS of a pointer are atomic. Items are distinct (tag them with the enqueue that made them). The specification is the sequential FIFO queue, where dequeue returns EMPTY on an empty queue.

Part 1: one CRQ

Fix a CRQ CC with ring size RR. An enqueue attempt with ticket tt is one round of lines L1 to L6 whose fetch-and-add returned tt with the bit open; a dequeue attempt with ticket hh is one round of lines L7 to L14. Tickets are distinct for each counter, since fetch-and-add hands out each value once. A cell ii serves tickets i,i+R,i+2R,…i, i + R, i + 2R, \ldots; its round is its idxidx.

Lemma 1 (cells move forward). Every successful CAS2 on cell ii keeps idx≡i(modR)idx \equiv i \pmod R and either raises idxidx or keeps idxidx and changes (safe,val)(safe, val) in one of two ways: ⊥→x\bot \to x with safesafe set to 1 (line L5, which may also raise idxidx), or safesafe from 1 to 0 with the same item (line L11). A cell never returns to a state it had before, so a CAS2 that succeeds compares against the state that was read, with no ABA.

Proof. Line L5 replaces (s,idx,⊥)(s, idx, \bot) by (1,t,x)(1, t, x) with idx≤tidx \le t and t≡it \equiv i. Lines L10 and L12 set idxidx to h+R>idxh + R > idx, since idx≤hidx \le h there (line L9 exits otherwise). Line L11 only clears safesafe. For a fixed idxidx, the state can go from ⊥\bot to an item once (afterwards line L5 needs ⊥\bot, and the only way back to ⊥\bot is lines L10 and L12, which raise idxidx), and from safe=1safe = 1 to 00 once. So the sequence of states of the cell is strictly increasing in the order (idx, then val set, then safe cleared, the latter two in either order), and no state repeats. □\square

Lemma 2 (who takes an item). An item stored by the enqueue attempt with ticket tt (line L5, giving idx=tidx = t) is removed from its cell only by line L10 of the dequeue attempt with ticket tt.

Proof. Only line L10 removes an item, and it requires idx=hidx = h. After line L5 the idxidx stays tt until the item is removed (only lines L10 and L12 raise it, and line L12 needs ⊥\bot). So h=th = t. □\square

Lemma 3 (tickets match). Let EE be an enqueue attempt with ticket tt and let DD be the dequeue attempt with ticket tt, if any. (a) If EE stores its item, then DD, if it runs line L8 at all, returns that item. (b) If DD exits its inner loop without an item, then EE does not store, either before or after.

Proof. Consider the moment when cell c=t mod Rc = t \bmod R first has idx>tidx > t, or first holds the item of ticket tt; one of these happens before either attempt can succeed with ticket tt, and by Lemma 1 they cannot both precede each other.

(a) Suppose EE stored, so c=(⋅,t,x)c = (\cdot, t, x) from that CAS2 on. By Lemma 1 the idxidx of cc stays tt until xx is removed, and by Lemma 2 only DD removes it. When DD reads the cell, either the item is there with idx=t=hidx = t = h, and line L10 is tried; a failed CAS2 means the cell changed, which only DD itself or line L11 can cause (clearing safesafe keeps idxidx and xx), so DD retries and eventually succeeds, since line L11 succeeds at most once. Or the cell is earlier than EE's CAS2 in Lemma 1's order, with idx≤tidx \le t; then DD tries line L11 or L12. Line L12 with idx≤tidx \le t and ⊥\bot would raise idxidx above tt before EE's CAS2, which then fails, a contradiction; so that CAS2 of DD fails, and DD rereads. Line L11 needs an item of an older round, idx<tidx < t. A success of line L11 makes DD give up, so we must exclude it: after it, the cell holds an item with idx<tidx < t and safe=0safe = 0, and when its dequeuer takes it, the cell becomes (0,t,⊥)(0, t, \bot) (line L10 gives idx+R=tidx + R = t, the only round between). Now EE can store only through line L4 with s=0s = 0, which needs head≤thead \le t; but DD took ticket tt before line L11, so head>thead > t from then on. So EE cannot store after the success of line L11, and it did not store before it, since the cell then held an older item. This contradicts the assumption that EE stored, so line L11 does not succeed for DD, and DD returns xx.

(b) DD exits in three ways. By line L9, the cell had idx>tidx > t; by Lemma 1 it keeps idx>tidx > t, and EE's line L4 or L5 fails from then on; and EE did not store before, since then idxidx would have stayed tt until DD removed the item. By line L12, the same holds from that CAS2 on. By line L11, the argument of (a) shows that EE cannot store afterwards, and it did not store before, since the cell held an older item until then and line L5 needs ⊥\bot. □\square

Lemma 4 (closed means closed). Once the closed bit of C.tailC.tail is set, every later attempt returns CLOSED at line L2 and stores nothing. FIX-STATE never clears the bit.

Proof. The bit is cleared by no step: fetch-and-add keeps it, and line L15 CASes only from an open tail to an open tail. □\square

Lemma 5 (empty). If a dequeue attempt with ticket hh reads at line L13 a tail counter tε≤h+1t_\varepsilon \le h + 1 at time τ\tau, then every enqueue attempt that has taken a ticket tt before τ\tau has t≤ht \le h, and each such attempt with t<ht < h has a dequeue attempt with ticket tt that took its ticket before τ\tau.

Proof. The tail counter only grows, except at line L15, which sets it to the head counter when that is larger. The tickets handed out so far are below the current counter tεt_\varepsilon, so t≤ht \le h. The head counter was above h≥th \ge t at τ\tau, so every ticket below hh has been taken by a dequeue attempt. □\square

Linearization of a CRQ. Call an enqueue attempt successful if it stores. Define the points:

  • A successful enqueue attempt with ticket tt: L(Et)L(E_t) = its fetch-and-add at line L1. For a CRQ created at line L18 with its first item, the item has ticket 0 and its point is defined in Part 2.
  • A dequeue that returns the item of ticket tt: L(Dt)=max⁡(FtD,FtE,L(Dt′))L(D_t) = \max(F^D_t, F^E_t, L(D_{t'})), where FtDF^D_t and FtEF^E_t are the fetch-and-adds of the two attempts and t′t' is the largest successful ticket below tt; when several points coincide, enqueues come first, then dequeues in ticket order.
  • A dequeue that returns EMPTY: its read of the tail at line L13, at time τ\tau.

Each point is inside its call: L(Dt)L(D_t) is at or after FtDF^D_t, and before the return of DtD_t, since DtD_t returns only after EtE_t stored (after FtEF^E_t) and, by induction, after L(Dt′)L(D_{t'}) (the item of t′t' is taken by the attempt with ticket t′<tt' < t, which got its ticket before DtD_t; if Dt′D_{t'} has not returned yet, its point is at most the later of its own two fetch-and-adds and the point before it, all of which precede the return of DtD_t, as each of them is a fetch-and-add that handed out a smaller ticket than FtDF^D_t or FtEF^E_t, and so happened earlier).

Theorem 1 (a CRQ is a linearizable queue that may refuse enqueues). In the order of the points, the enqueues are in ticket order, the dequeues that return items are in ticket order, each item is dequeued after it is enqueued, and at every EMPTY point all enqueued items have been dequeued. So the sequence is a legal history of a FIFO queue.

Proof. Enqueue points are fetch-and-adds on the tail, so they are in ticket order. Dequeue points are in ticket order by their definition. L(Dt)≥FtE=L(Et)L(D_t) \ge F^E_t = L(E_t), and ties put enqueues first. Since enqueues and dequeues of items are both in ticket order and Lemma 3 pairs each item with the dequeue of its own ticket, the kk-th dequeue returns the kk-th enqueued item: before L(Dt)L(D_t) all successful enqueues with tickets below tt are linearized (for t′′<tt'' < t, the fetch-and-add Ft′′EF^E_{t''} is before FtEF^E_t, since the tail hands out tickets in increasing order), and those with tickets above tt that are linearized before L(Dt)L(D_t) come after the item of tt in the queue. So DtD_t removes the front.

For an EMPTY at τ\tau with ticket hh: by Lemma 5, each enqueue linearized before τ\tau has ticket t≤ht \le h. Its ticket is not hh: the attempt with ticket hh cannot store by Lemma 3(b). For t<ht < h, the dequeue attempt with ticket tt took its ticket before τ\tau, and by Lemma 3(a) it returns the item; its point max⁡(FtD,FtE,L(Dt′))\max(F^D_t, F^E_t, L(D_{t'})) is before τ\tau by induction on tt, since both fetch-and-adds were before τ\tau. So every item enqueued before τ\tau is dequeued before τ\tau, and the queue is empty at τ\tau. If τ\tau ties with a dequeue point, that point is a fetch-and-add, which is a different step; there are no ties. Pending calls: a pending enqueue that stored is kept, and a pending dequeue whose ticket matches a stored item is completed with that item (Lemma 3(a)); the others are removed. □\square

Part 2: the list of CRQs

Lemma 6 (the list). (a) C.nextC.next changes once, from NIL, by line L18, and only after CC is closed. (b) Q.tailQ.tail and Q.headQ.head move only to the next CRQ in the list. (c) Q.headQ.head moves from CC only after some dequeue saw C.next≠C.next \ne NIL and then had an attempt on CC return EMPTY.

Proof. (a) Line L18 runs only after CRQ-ENQUEUE returned CLOSED, which happens after the bit was set, by the caller or another thread (lines L2 and L6). (b) and (c) by lines L16 and L22. □\square

The abstract queue. Let QQ be the concatenation, in list order from Q.headQ.head to the end, of the CRQ queues of Theorem 1. For a CRQ NN created at line L18 with item xx, linearize the enqueue of xx at the CAS of C.nextC.next that links NN; this is inside the call.

Theorem 2 (linearizability). The LCRQ is linearizable with respect to the sequential FIFO queue.

Proof. Use the points of Theorem 1 for each CRQ, and the CAS of C.nextC.next for first items. Every enqueue call is linearized once: its last attempt stored, or its CAS at line L18 linked its CRQ; earlier attempts returned CLOSED and did nothing (Lemma 4). Every dequeue call that returns an item is linearized at the point of its last attempt.

Items enter QQ at the end: an enqueue that stores into CC does so while CC is open, so while C.nextC.next is NIL by Lemma 6(a), and CC is then the last CRQ. A new CRQ is appended only after CC is closed, so no item enters CC afterwards. So the order of enqueue points is the order of items in QQ.

Items leave QQ at the front: a dequeue attempt on CC returns an item of CC, the front of CC by Theorem 1. All CRQs before CC in QQ are empty at that point: Q.headQ.head moved past each of them only after a drain (Lemma 6(c)) that returned EMPTY after their nextnext was set, so after they were closed, and a closed CRQ that was empty at a point stays empty (no more items enter it by Lemma 4). So the item is the front of QQ.

An LCRQ EMPTY is returned at line L20 after an attempt on CC returned EMPTY at its point τ\tau, and C.nextC.next was NIL at a later read. At τ\tau, CC is empty, CRQs before CC are empty as above, and there is no CRQ after CC, since C.nextC.next was still NIL later. So QQ is empty at τ\tau. Since all points are inside their calls, real time is respected. □\square

Part 3: progress

Theorem 3 (lock-freedom). In every infinite execution in which some operation is pending, infinitely many operations finish.

Proof. Suppose that from some moment on no operation finishes. Each attempt of CRQ-ENQUEUE or CRQ-DEQUEUE finishes in a bounded number of its own steps, except for the retries of the inner loop of lines L8 to L12 and of FIX-STATE. A retry of lines L8 to L12 happens only after a CAS2 failed, so after another CAS2 on the same cell succeeded, and by Lemma 1 a cell has at most two changes per round before its round grows; a round grows only through a dequeue ticket of that round, and tickets are only handed out by fetch-and-adds, each a step of an attempt. FIX-STATE retries only if the tail changed, which also needs another step to succeed. So infinitely many attempts are made.

Enqueue side: each CRQ-ENQUEUE call returns CLOSED after at most patiencepatience failed tries (line L6), so each call makes finitely many tries per CRQ. If a call gets CLOSED, it either links a new CRQ and finishes, or its CAS of line L18 fails because another call linked one, and that call then finishes after one more step, or Q.tailQ.tail moves on and it tries the new CRQ. So infinitely many enqueue tries without a finished enqueue are impossible: with finitely many threads, some enqueue would close a CRQ and link a new one, and finish.

Dequeue side: a dequeue attempt that does not return an item finds t>h+1t > h + 1 at line L13 and retries; this means an enqueue attempt took a ticket above h+1h + 1. Infinitely many such retries need infinitely many enqueue tickets, which the enqueue side excludes from some moment on (no enqueue tries without finished enqueues means no tickets). After that, the next attempt of a dequeue sees t≤h+1t \le h + 1 and returns EMPTY, or returns an item. At line L20, a dequeue either returns EMPTY or moves to the next CRQ; there are finitely many CRQs, since no new one is linked, so it finishes. Both contradict the assumption. ■\blacksquare

Back to LCRQ (linked concurrent ring queue)