Algorithm Search⌘K

Proofs: B-link tree

These are the human proofs of B-link tree, at full length.

The B-link tree is a linearizable, deadlock-free map with lock-free searches full

This proof shows: correctness, linearizability, progress.

We use the pseudocode of the page. A READ or WRITE of a node is one atomic step on its whole page, and so are the reads and writes of the anchor (the root and the height). LOCK and UNLOCK are those of a mutex. Keys come from a totally ordered set, extended by −∞-\infty and +∞+\infty.

Writers. Every WRITE of an existing node nn is done by the holder of nn's lock, and the write of a new node uu at line L14 happens before any pointer to uu is written. So while a thread holds the lock of nn, the page it read after locking is the current page of nn, and it stays current until this thread writes it.

Low bounds. Give each node a fixed low bound: −∞-\infty for the first leaf and for each new root, and low(u)=ylow(u) = y for the node uu created by a split at line L14, where yy is the new high key of the node that split. A node is in the chain of its level once a pointer to it has been written.

Lemma 1 (the chain invariant). At every moment, for each level LL up to the height:

  1. The nodes of level LL in the chain, from the leftmost node of the level along the right links, are n1,n2,…,nrn_1, n_2, \ldots, n_r with low(n1)=−∞low(n_1) = -\infty, low(nj+1)=high(nj)low(n_{j+1}) = high(n_j) and high(nr)=+∞high(n_r) = +\infty, so their ranges (low(nj),high(nj)](low(n_j), high(n_j)] partition the keys.
  2. Every key of a page lies in the range of its node (for an internal node, every separator except the last lies inside the range, and the last equals the high key).
  3. For an internal node with entries (m1,c1),…,(ms,cs)(m_1, c_1), \ldots, (m_s, c_s): low(c1)=low(n)low(c_1) = low(n), and low(ci)=mi−1low(c_i) = m_{i-1} for i>1i > 1; each cic_i is a node of the level below in the chain.

Proof. We prove Lemma 1 and Lemma 2 together, by induction on the steps of the execution: Lemma 2 for walks up to a step uses Lemma 1 for the states before it, and Lemma 1 after a write uses Lemma 2 for the walk of the writer. Initially there is one leaf with range (−∞,+∞](-\infty, +\infty]. We check every write.

A write at line L9, L13 or L20 of a node nn changes its keys but not its high key or link. At line L13 the inserted key yy is in the range of nn: the thread reached nn with y>low(n)y > low(n) (Lemma 2 below) and moved right until y≤high(n)y \le high(n). At an internal node, the entry (m,c)(m, c) with the smallest m≥ym \ge y becomes (y,c),(m,u)(y, c), (m, u); part 3 still holds for cc, and low(u)=ylow(u) = y is the new separator before uu. So 2 and 3 hold. Deletions only remove keys.

The write of uu at line L14 changes no node in the chain. The write of nn at line L14 replaces the page AA, with range (low(n),h](low(n), h] and link ℓ\ell, by A′A' with high key y=max⁡(A′)y = \max(A') and link uu, while uu holds BB with high key hh and link ℓ\ell. So in the chain, nn is replaced by n,un, u with ranges (low(n),y](low(n), y] and (y,h](y, h], and part 1 holds. The keys of AA that are at most yy are in A′A', and the others in BB, so part 2 holds. If AA is internal, BB starts with an entry whose child has low bound equal to the separator before it in AA, which is y=low(u)y = low(u), so part 3 holds for uu, and it is unchanged for nn.

A new root with entries (y,old)(y, old) and (+∞,u)(+\infty, u) is made only if oldold is the root, so oldold is the leftmost and, before its split, only node of its level: then the level is old,uold, u, and the new root satisfies 1 to 3 with range (−∞,+∞](-\infty, +\infty]. The height grows by one at the same step. □\square

Lemma 2 (routing). Suppose a thread at level LL reads node nn while looking for key kk with k>low(n)k > low(n). If k>high(n)k > high(n) in the page read, the link n′n' satisfies k>low(n′)=high(n)k > low(n') = high(n). If k≤high(n)k \le high(n) and nn is internal, the chosen child cic_i satisfies k>low(ci)k > low(c_i). So every node that SEARCH, the descents of lines L7, L16 and L19, and MOVE-RIGHT visit has k>lowk > low, and the last node read covers kk at the moment of its read.

Proof. The first case is part 1 of Lemma 1. In the second, mi−1<k≤mim_{i-1} < k \le m_i, and low(ci)=mi−1low(c_i) = m_{i-1} by part 3, or c1c_1 has low(c1)=low(n)<klow(c_1) = low(n) < k. Every walk starts at the root, of low bound −∞-\infty. A walk stops at level LL only when k≤high(n)k \le high(n) in the page just read, and with k>low(n)k > low(n) the node covers kk at that read. Since low bounds never change, k>low(n)k > low(n) stays true after the read. For the move right at the parent with key yy in line L17: the popped node pp was visited at the level above on the way to the leaf, so k>low(p)k > low(p); the leaf nn that split had low(n)≥low(n) \ge the low bound of the node reached from pp, which is at least low(p)low(p), and y=max⁡(A′)y = \max(A') with A′A' nonempty and inside (low(n),…](low(n), \ldots], so y>low(p)y > low(p). The same holds by induction at each higher level. □\square

The abstract map. At each moment, the map MM is the set of pairs in the pages of the leaves in the chain. By part 1 and 2 of Lemma 1, each key is in at most one leaf, the one whose range holds it.

Theorem (linearizability). Each history of the B-link tree is linearizable with respect to the sequential ordered map.

Proof. Choose the points:

  • a search: its last READ, at line L2 or L5;
  • an insert that finds kk present: its WRITE at line L9; an insert of a new key: its WRITE of the leaf, at line L13 or at the second WRITE of line L14;
  • a delete that finds kk: its WRITE at line L20; a delete that does not: the READ of the locked leaf after MOVE-RIGHT.

Each point is inside its call, so the order of the points respects real time; pending calls past their point are completed, the others removed.

Search: by Lemma 2 its last page read is of the leaf that covers kk at that moment, so the value it returns is M(k)M(k) then, or NIL.

Updates: by Lemma 2 the leaf nn locked after MOVE-RIGHT covers kk, and since the thread holds the lock, its page is the current page and nn keeps covering kk until the thread writes it (only a split of nn, by a lock holder, could shrink its range). An insert that finds kk changes only the value of kk in MM and returns the old value. An insert of a new key at line L13 adds (k,v)(k, v) to the leaf that covers kk. At line L14, the first WRITE, of uu, changes no leaf in the chain, so it does not change MM; the second puts A′A' and BB in the chain, whose pairs are the old pairs of nn plus (k,v)(k, v). So MM gains exactly (k,v)(k, v), and the insert returns NIL as it should. All later steps of the insert act on internal nodes or the anchor and do not change MM. A delete removes kk from the leaf that covers it and returns its value; a delete that does not find kk in the current page of the covering leaf returns NIL with M(k)M(k) undefined at that moment. Every write of a leaf page is one of these points, so MM changes only at linearization points, each by the change of the sequential map. □\square

Theorem (progress). Searches take no locks and never wait. Updates are deadlock-free: if threads keep taking steps, some update that has started finishes, and with finitely many updates every operation finishes.

Proof. Order the nodes by (level, low bound). This order is fixed, since neither changes, and it is total among nodes of the chain, since low bounds on a level are distinct by part 1 of Lemma 1. An update that holds locks only waits for a lock that is higher in the order: at line L18 it holds nn and waits for the link, of a larger low bound on the same level; at line L17 it holds oldold and waits for a node one level up. So a cycle of threads waiting for locks is impossible. The anchor is held only inside one atomic step, so nobody waits for it. The only other wait is the loop of line L16 when the top level has a node oldold that is not the root: then the root rr has split, its new right part is in the chain, and the thread that split rr holds the lock of rr until it makes the new root, which it does at its next step with the anchor without waiting for anything. So every waiting thread waits for a thread that is not waiting, or for one that waits for a higher lock, and the highest one in a chain of waits makes progress.

Each update does at most one split per level, and the number of levels grows only by root splits, so an update that keeps getting locks finishes after a bounded number of its own steps plus the moves right. A walk at level LL moves right only past nodes that exist in the chain between its node and the one that covers kk; each move right passes a node created by a split, and with finitely many updates there are finitely many splits, so every walk ends. ■\blacksquare

Back to B-link tree