Algorithm Search⌘K

Proofs: Combining tree

These are the human proofs of Combining tree, at full length.

The combining tree is a linearizable, deadlock-free counter full

This proof shows: correctness, linearizability, progress.

We use the pseudocode of the page. Each node procedure runs under the monitor of its node, so it is atomic, except that "wait until" releases the monitor while the condition is false and holds it from the moment the condition is true until the procedure ends. Concurrent callers have distinct ids, so at most two threads start at each leaf at any time. Depth is measured from the root, which has depth 0.

Rounds at a node. Fix a non-root node nn. A round at nn starts when a thread AA sets n.statusn.status from IDLE to FIRST at line L10 and ends when n.statusn.status is IDLE again. We call AA the active thread of the round. If some thread BB sets FIRST to SECOND at line L11 during the round, BB is its passive thread.

Lemma 1 (one pair per round). In each round at nn there is one active thread and at most one passive thread, and the passive thread, if any, arrives before the active thread combines at nn. The round ends at line L20 if there is no passive thread, and at line L19 otherwise. During a round no other thread passes line L9 or line L13 at nn except as stated.

Proof. Line L10 needs status IDLE, so only one thread starts the round, and line L11 needs status FIRST, so at most one passive thread comes. The flag n.lockedn.locked is true from line L11 of the passive thread until its line L17, and again from line L14 of the active thread until the end of the round (line L20, or line L19 of the passive thread). The active thread passes line L13 only when the flag is false: either before any passive thread came, and then line L14 sets the flag with status FIRST, so no passive thread can come until line L20 ends the round; or after line L17 of the passive thread.

It remains to show that no third thread TT runs line L10 to L12 at nn during the round, which we prove by induction on the height of nn. A thread reaches nn either at the start of a call, if nn is its leaf, or after it was active at a child of nn. If nn is a leaf, the only threads that start there are the two with ids 2i2i and 2i+12i + 1, so TT is AA or BB in a later call. BB returns only at line L19, which ends the round. AA returns after line L20, which ends the round, or after line L21, which leaves status RESULT and the flag set, so a new call of AA waits at line L9. If nn is not a leaf, AA and BB came from children $c_A e c_B$ of nn, where their own rounds are in progress: the round of BB at cBc_B lasts until BB distributes there, after line L19 at nn, and the round of AA at cAc_A lasts until AA distributes there, after it has distributed at nn. By the induction hypothesis no third thread becomes active at cAc_A or cBc_B during these rounds, so TT cannot come from there before AA has distributed at nn, and after that the flag is set until the round ends. □\square

Lemma 2 (what a thread carries). Say that a thread TT covers itself and, for each node nn at which TT combined as the active thread with status SECOND, all threads covered by the passive thread of nn in its call. Then the value combined that TT passes to line L6 is the number of threads it covers, the sets covered at different nodes are disjoint, and a thread is covered only by threads of the same batch, defined below.

Proof. By induction on the climb of TT. At the start combined =1= 1, for TT itself. At line L15 at node nn, TT adds n.secondn.second, which the passive thread BB of the round wrote at line L17 as its own combined value, so by induction on BB (whose climb ended at nn, below TT's stop node, and happened earlier) it is the number of threads that BB covers. Line L14 records the old value of combined in n.firstn.first. The sets are disjoint because they come from different subtrees: BB climbed only in the subtree of the child of nn from which it came, and each thread is counted by the one active thread that combined with it. □\square

Batches. A call that runs line L16 at the root is the head of a batch; the batch is the set of threads that the head covers, of size cc. The head's root step adds cc to the counter and gets prior.

Lemma 3 (distribution). The threads of a batch return the values prior, prior +1+ 1, …\ldots, prior +c−1+ c - 1, each value once.

Proof. We show more: if a thread TT gets value qq at line L6 and covers kk threads, then the threads it covers return q,…,q+k−1q, \ldots, q + k - 1, with TT itself returning qq. TT returns qq at line L8. TT combined at its nodes from the leaf up, and at the node nn of its jj-th combine it had already covered n.firstn.first threads, those of the nodes below nn and itself. The passive thread BB of nn covers n.secondn.second threads. Line L21 gives BB the value q+n.firstq + n.first, and BB returns it from line L19, since the passive thread reads n.resultn.result in the same atomic block that ends its wait. By induction on BB, the threads BB covers get q+n.first,…,q+n.first+n.second−1q + n.first, \ldots, q + n.first + n.second - 1. The nodes of TT in order from the leaf give consecutive blocks that start right after the block of the nodes below, so the blocks of TT, from the leaf up, are {q}\{q\} and then consecutive ranges that fill q+1,…,q+k−1q + 1, \ldots, q + k - 1. □\square

Theorem (linearizability). Each history of the combining tree is linearizable with respect to the sequential counter.

Proof. The root steps of different batches are atomic steps at the root monitor and are totally ordered. Each one adds the size of its batch to the root result, so by Lemma 3 the batches get disjoint ranges of values, in the order of their root steps, and all values from the initial count up are used. Linearize every call of a batch at the root step of the batch, ordered by value within the batch. Take any call XX of the batch. XX is pending at this step: it started before, since it has precombined and deposited its increment (or is the head), and it has not returned, since it returns only after the head has distributed, which is after the root step. So each linearization point is inside its call's interval, and the order of points respects real time. In this order, the kk-th call gets the initial count plus k−1k - 1, which is the sequential specification. A pending call that has not reached the root step of its batch is removed from the completion; a call whose batch has done its root step is completed with the value given by Lemma 3. □\square

Theorem (deadlock-freedom). If threads keep taking steps and some call is pending, some call returns.

Proof. Classify the waits. (a) An active thread at line L13 of node nn waits for the passive thread BB of nn to run line L17 (Lemma 1). (b) A passive thread at line L18 of nn waits for the active thread of nn to run line L21. (c) A thread at line L9 of nn waits for the end of the current round at nn.

Claim 1. Each thread that has finished precombining reaches line L6. Its combines are at nodes on its path below its stop node. A wait (a) at depth dd is for a thread BB whose remaining combines are at depths greater than dd, after which BB runs line L17 with no further wait. By induction on depth, from the deepest nodes up, each such wait ends.

Claim 2. Each thread that has reached line L6 returns. At the root, line L16 has no wait, and line L20 and line L21 have none. A passive thread at stop node nn of depth d≥1d \ge 1 waits (b) for the active thread AA of nn. By Claim 1, AA reaches line L6 at its stop node, of depth less than dd. By induction on the depth of the stop node, from the root down, AA returns, and it runs line L21 at nn on its way, which ends the wait of the passive thread, which then returns.

Claim 3. Suppose no call returns from some moment on. By Claims 1 and 2, each thread that has finished precombining returns, so from that moment on no thread has finished precombining: every pending thread is in its precombining phase. A flag is set only by a thread that has finished precombining (lines L11 and L14) and cleared by such threads before they return, so then every flag is false, no thread waits at line L9, and each pending thread finishes precombining within O(log⁡w)O(\log w) of its own steps. This contradicts the assumption. ■\blacksquare

Back to Combining tree