Proofs: Concurrent queue
These are the human proofs of Concurrent queue, at full length.
The one-lock queue is linearizable and deadlock-free full
This proof shows: correctness, linearizability, progress.
We use the pseudocode of the page. A history is a finite sequence of call and return events of the threads. A call is complete if its return is in the history and pending otherwise. The lock satisfies mutual exclusion: at each moment at most one thread holds it. We say that a call enters when it completes line L2 or line L6 (the lock is now held by it), and that it leaves when it executes line L5, line L11 or the release of line L8.
Lemma 1 (one at a time). The critical sections of the calls are disjoint in time and are totally ordered. A call that enters leaves after of its own steps, and it changes , and the next field of a node only while it holds the lock.
Proof. Mutual exclusion gives disjointness, and disjoint intervals of a line are totally ordered. Each critical section is straight-line code of at most four steps (lines L3 and L4, or lines L7 to L11). Lines L3, L4 and L9 are the only writes to , or a next field of a node in the list, and each one is inside a critical section. Line L1 writes the fields of a new node that no other thread can reach until line L3 links it.
Number the critical sections in the order of Lemma 1, and let be the call that runs . Let be the sequential history of the queue type, with the arguments of the calls, and let be the queue state after (with empty).
Lemma 2 (the invariant). For each , after leaves (for , at the start): the list from is finite, is its last node, the values after are , and the result of is the result of its call in .
Proof. By induction on . For the constructor makes one sentinel node that is both and , with no node after it, and is empty.
Let and assume the claim for . Between the end of and the start of no thread holds the lock, so by Lemma 1 nothing changes the list, or . So when starts, the claim for still describes the list. By Lemma 1, runs alone, so its steps act on this state as sequential code does.
- If is enq: line L3 sets the next field of the last node to the new node , whose next is nil, and line L4 makes . The list is still finite, is its last node, and the values after are . The call returns nothing, as in .
- If is deq and is empty: no node follows , so line L7 reads nil and line L8 returns EMPTY without a write. The state is , and EMPTY is the sequential result.
- If is deq and : line L7 reads the node that holds . Line L9 makes the new head, so the values after are . The tail does not change. It is still the last node, and it is still reachable: if was the last node, then is now the sentinel. Line L10 reads , the sequential result.
Theorem (linearizability). Each history of the one-lock queue is linearizable with respect to the sequential queue type.
Proof. First remove each pending call from . This changes no result of a complete call. A pending call that has not entered has changed nothing that another thread can read (line L1 writes only a new node that is not linked). A pending call that has entered holds the lock until the end of , so its critical section is the last one, and no other call enters after it and reads what it wrote. So the complete calls are for some , and Lemma 2 applies to each of them.
Let the linearization point of each complete call be the moment it enters. This moment lies after the call starts, and before it returns, because the call leaves after it enters and returns after it leaves. The order of the linearization points is the order of Lemma 1. So the sequential history in this order keeps the real-time order of : if returns before starts, the point of is before the return of , which is before the start of , which is before the point of . By Lemma 2, each call has in the result it has in . So is a legal sequential queue history with the same calls and results as the completed , in an order that extends the real-time order.
Theorem (progress). If the lock is deadlock-free, then the queue is deadlock-free: if some call is pending and no thread stops, then some call returns. If the lock is starvation-free, then each call of a thread that does not stop returns.
Proof. A thread holds the lock for at most four of its own steps (Lemma 1), and then releases it on each path, including the early return of line L8. So a thread that holds the lock and keeps taking steps releases it. If calls wait at line L2 or L6, deadlock-freedom of the lock gives that some waiting call enters; it then leaves and returns after steps. With a starvation-free lock each waiting call enters, and then returns.
The queue is not lock-free: if the thread that holds the lock stops, for example because it is preempted for ever, then no other call can return.