Proofs: Combining funnel
These are the human proofs of Combining funnel, at full length.
The combining funnel counter is linearizable, and every call finishes when all threads keep running full
This proof shows: correctness, linearizability, progress.
We use the pseudocode of the page. SWAP of a cell, CAS and writes of a word, reads and writes of a word, and the FETCH-AND-ADD of the counter are atomic. The fields and of a record are written only by its owner. The specification is the sequential counter: fetch-and-add returns the current value and sets it to .
Fix a call of thread . Between its line L1 and its return, runs no other call, so we speak of the record of .
Lemma 1 (who changes ). During , is written by only at lines L1, L5, L7 and L9, and by another thread only by the CAS of line L6, which changes it from a layer number to CAPTURED. Once it is CAPTURED, it stays CAPTURED until returns.
Proof. By inspection. Lines L5 and L9 are CASes that expect a layer number, and line L7 runs only after line L5 succeeded, so is SELF just before it, and nobody else changes SELF (line L6 expects a layer number). So none of the writes of replaces CAPTURED, and other threads only write CAPTURED.
Lemma 2 (captures). Say that captures when the CAS of line L6 of a call of on succeeds during . Then is captured at most once. If it is captured, it does not reach line L10, and from the capture on, and do not change. If it is not captured, it reaches line L10.
Proof. Only one CAS can change from a layer number to CAPTURED, since it then stays CAPTURED (Lemma 1). After the capture, the next CAS of at line L5 or line L9 fails, as is not a layer number, and goes to line L11. Changes of and happen only at line L6 of , which requires a successful CAS at line L5 in the same round, and is SELF from that CAS until line L7, so it cannot be captured in between, and no change of happens after the capture. A capture can happen only while is a layer number, that is, before line L10, since line L9 for sets to , and no CAS at line L6 expects . If is never captured, every CAS of at lines L5 and L9 succeeds, and it reaches line L10.
So the captures form a forest: each captured call has one parent, its capturer. A capturer reads the sum of its kid after the capture (line L6), so the value it adds is the final sum of the kid. The forest has no cycle, since a call can capture only while it is not captured, and a captured call no longer captures.
Lemma 3 (sums and ranges). For each call with amount , at the moment it reaches line L10 or is captured, its final sum is plus the amounts of all calls in its subtree. Its kids, in the order of , have offsets with , and , and .
Proof. By induction on the height of the subtree. At line L1 the sum is and there are no kids. Each successful line L6 appends the kid with offset equal to the current sum, and then adds the final sum of the kid (Lemma 2). So the offsets are the prefix sums of followed by the sums of the kids, and the final sum is plus the sums of the kids, each of which, by induction, is the sum of the amounts of its subtree.
Definition (ranges). Give each call a range of counter values. A root, the call that reaches line L10, gets , where is the value returned by its FETCH-AND-ADD. A kid at offset of a call with range starting at gets the range that starts at and has length equal to its sum. By Lemma 3, the ranges of the kids of a call and the first values of its own range are disjoint and fill the range of the call.
Lemma 4 (results). Each call returns the start of its range.
Proof. A root returns (line L13). A captured call returns the value written into its by its capturer at line L12, which is , where is the value that the capturer returns, by induction on the depth: the capturer returns the start of its own range. The result word of is reset at line L1, before is set to 0, so before any capture of , and it is written once, by the parent.
Theorem (linearizability). Each history of the funnel is linearizable with respect to the sequential counter.
Proof. Each root performs exactly one FETCH-AND-ADD of at line L10. Every call in the subtree of is pending at that step: it was invoked before it was captured, and it returns only after it reads its result, which is computed from , so after the step. Linearize all calls of the subtree at this step, in increasing order of the start of their ranges. By Lemma 3 these ranges tile , each starting where the previous one ends after its own amount, so in this order, the call that starts at sees the counter at and adds its own amount , and the next call starts at .
Here we must check that the ranges of a subtree, ordered by start, are such that the call with range start and amount is followed by the call with start . Inside the range of a call with start , the call's own amount takes , and its kids' ranges follow in order, each tiled recursively in the same way. So the order by start lists, for each call, first the call and then its kids' subtrees, each in this order; this is a preorder of the tree, and consecutive starts differ by the amount of the earlier call. So the sequence of calls, replayed on a counter that holds , returns the start of each range, which by Lemma 4 is what each call returned.
The FETCH-AND-ADD steps of different roots are atomic steps on the counter, so they are ordered, and between two of them the counter does not change. Each root's step adds the total amount of its subtree. So the sequence of all linearized calls, root step by root step, is a legal sequential history. The points are inside the calls, so the order respects real time. Pending calls that are in the subtree of a root that has done its step are completed with their results; the others are removed.
Theorem (progress). No call waits for a call that it captured. If every thread keeps taking steps, every call finishes. A call that is never captured finishes within of its own steps, where is the wait of line L8.
Proof. The only waiting is at line L11, for the result from the capturer. A root never waits: it passes each layer in at most a constant number of steps plus , applies line L10, and writes the results of its kids in steps. A call captured by waits until reaches line L12; was not captured when it captured, so either is a root or is captured later by a call of a thread that is still running. Along the chain of capturers, each one was captured after the one before captured it, and the chain ends at a root, which finishes by the above if its thread keeps taking steps; then the results flow down the chain, each step of which is one write. So every call finishes. The funnel is not lock-free: a capturer that stops for ever blocks the calls it captured.