Proofs: Two-lock queue
These are the human proofs of Two-lock queue, at full length.
The two-lock queue is linearizable and deadlock-free full
This proof shows: correctness, linearizability, progress.
We use the pseudocode of the page. The reads and writes of a next pointer, of and of are atomic, and a node gets its value at line L1 before any other thread can reach it. Each lock gives mutual exclusion. Let be the list of nodes reachable from by next pointers, and let be the sequence of the values of the nodes of after . We call an enqueuer that has executed line L3 but not line L4 open.
Lemma 1 (who writes what). Only an enqueuer that holds writes a next pointer of a node in (line L3) or (line L4). Only a dequeuer that holds writes (line L11). So at most one thread is open at a time.
Proof. These are the only writes of the code to these fields, and each is between the acquire and the release of the named lock. Line L1 writes the fields of a new node that is not yet in . An open enqueuer holds , so there is at most one.
Lemma 2 (shape). At each moment: (a) is finite and acyclic and its last node has next nil; (b) if no enqueuer is open, ; (c) if an enqueuer is open, is the node that linked, and is the predecessor of , which is either in or was the head just before the head moved to .
Proof. By induction on the steps of the execution. At the start is the sentinel alone, , and no enqueuer is open. Only the writes of lines L3, L4 and L11 can change the claims (Lemma 1).
- Line L3 of . holds and no enqueuer is open, so by (b) and $\ell.next = $ nil. The write sets , a new node with next nil. So grows by at the end, stays acyclic, is the last node, and is its predecessor. Now is open and (c) holds.
- Line L4 of . is open, so by (c) . The write sets , and is no longer open, so (b) holds. does not change.
- Line L11 of a dequeuer . holds , so has not changed since read it at line L7, and new_head is the value of that read at line L8. Next pointers in are written only from nil to a new node (line L3), so this pointer still equals new_head, and new_head nil by line L9. So the write moves the head one node forward along . loses its first node, is still finite and acyclic, and does not change, because new_head was in . Claim (b) still holds if no enqueuer is open. If is open and the old head was , the predecessor of , then the new head is , and was the head just before; this is the second case of (c).
Lemma 3 (the atomic effects). Define the linearization point of each call: line L3 for an enqueue; line L8 for a dequeue that returns EMPTY; line L11 for a dequeue that returns an item. Then (a) at the point of enq, abs changes from to ; (b) at the point of a dequeue that returns EMPTY, abs is empty; (c) at the point of a dequeue that returns , abs changes from to ; (d) no other step changes abs.
Proof. (a) By the case L3 of Lemma 2, line L3 appends the node with value at the end of , and the head does not change, so abs becomes . (b) At line L8 the dequeuer reads , which is the current head because it holds . It reads nil, so is the head alone and abs is empty. (c) At line L10 the dequeuer read as the value of new_head, which is the first node after the head by the case L11 of Lemma 2; values never change after line L1 except on the new sentinel after line L11, where they are no longer part of abs. Line L11 removes the old head, so the first node becomes the head and abs loses exactly its first item . (d) The only steps that change or the head are lines L3 and L11 (Lemma 1 and Lemma 2); line L4 changes only , which abs does not use.
Theorem (linearizability). Each history of the two-lock queue is linearizable with respect to the sequential queue.
Proof. Take an execution and its history . Each linearization point of Lemma 3 is a step of the call itself, so it lies between the call and its return. Complete as follows: keep each pending call that has passed its linearization point, with the result that Lemma 3 gives (for an enqueue, none; for a dequeue at line L11, the value read at line L10), and remove the other pending calls. A removed call has made no change to abs, by Lemma 3 (d). Order the kept calls by their linearization points, which are distinct steps. By Lemma 3, applying the calls in this order to the empty sequence gives at each point exactly the change and the result of the sequential queue, so the sequential history is legal and each call has the same result in as in . If call returns before call starts, the point of is before the point of , so keeps the real-time order of .
Theorem (deadlock-freedom). If both locks are deadlock-free, then whenever a call is pending and the threads keep taking steps, some call returns. With starvation-free locks, each call returns.
Proof. No thread holds both locks, so no cycle of waits exists. A thread holds a lock for at most four of its own steps and releases it on each path, including the early return of line L9. So if calls wait at a lock, the holder releases it, and by deadlock-freedom of the lock some waiter enters; it then returns after more steps. With starvation-free locks each waiter enters.
Freeing nodes. When a dequeuer returns, the old head node is no longer in . No enqueuer reads it again: by Lemma 2 (c), it can be only while an enqueuer is open, and an open enqueuer performs only line L4 and line L5, which do not read the old tail. No other dequeuer reads it, because the next dequeuer reads the new head. So the node can be freed at line L13.