Algorithm Search⌘K

Proofs: Ctrie (concurrent hash-array mapped trie)

These are the human proofs of Ctrie (concurrent hash-array mapped trie), at full length.

The Ctrie is a linearizable, lock-free map full

This proof shows: correctness, linearizability, progress.

We use the pseudocode of the page, without snapshots. A READ and a CAS of the word mainmain of an I-node are atomic, and CAS compares by identity. All other nodes are immutable once created. For a hash hh, write hdh_d for the group (h≫dW) mod 2W(h \gg dW) \bmod 2^W; a node is at depth dd if it is reached from the root through dd C-node branches.

Reachability. An I-node, C-node, S-node, T-node or L-node is reachable if it can be reached from the root: from an I-node to its mainmain, from a C-node to its branches, from a T-node to its S-node, from an L-node to its S-nodes. The path of a reachable node is the sequence of groups of the C-node branches on the way.

The abstract map. MM is the set of pairs (key, value) of the reachable S-nodes.

Lemma 1 (final tombs). Once the mainmain of an I-node is a T-node, it never changes again.

Proof. Every CAS on a mainmain expects the C-node or the L-node that was read before (lines L10, L13, L15, L21, L25, L26, L28), never a T-node. □\square

Lemma 2 (shape). At every moment: (a) each reachable I-node has exactly one path, and so does each reachable S-node; (b) a reachable S-node of key kk whose path has length dd has a path equal to hash(k)0,…,hash(k)d−1hash(k)_0, \ldots, hash(k)_{d-1}, and an S-node in an L-node has all bits of its hash equal to the bits of the path; (c) no key has two reachable S-nodes; (d) the root's mainmain is a C-node; (e) an I-node stops being reachable only at a successful CAS of line L26 or L28 that replaces it by the S-node of its T-node, and an I-node that is reachable and whose mainmain is not a T-node stays reachable with the same path until its mainmain becomes a T-node.

Proof. By induction on the successful CAS steps, which are the only steps that change any reachable node. Initially the root holds an empty C-node. Consider a successful CAS on i.maini.main from mm to m′m'. By (e) for the moment of the CAS, ii is reachable with a fixed path PP of length dd: either it is the root, or its mainmain is mm, which is not a T-node, and every CAS on it before kept it in place.

  • Line L10 adds the S-node ss at the group hash(s.key)dhash(s.key)_d of mm, which was empty in mm. The thread reached ii along the groups of hash(s.key)hash(s.key) (it chose each branch by FLAGPOS), so (b) holds for ss. Since the group was empty, no S-node of the key was under ii, and by (b) no S-node of the key exists elsewhere; so (c) holds.
  • Line L12 and L13: if b.key=s.keyb.key = s.key, the S-node of the key is replaced at the same place. Otherwise DUAL builds a fresh subtree with bb and ss, whose hashes agree on the groups below d+1d + 1, splitting them at the first group where they differ, or in an L-node if they agree on all bits. Fresh I-nodes become reachable with one path each. (b) and (c) hold.
  • Line L15 replaces the pair of the key in an L-node, or adds it, and line L25 removes it; both keep (b) and (c).
  • Line L21 removes the S-node of the key, and CONTRACT may turn a C-node with one S-node into a T-node of it: the S-node keeps its path, since a T-node is entered without a new group. Not at the root, since lev>0lev > 0 is required, so (d) holds.
  • Line L26 replaces each branch I-node jj of mm whose node was a T-node at the moment COMPRESS read it by that T-node's S-node t.snt.sn. By Lemma 1, j.mainj.main is still that T-node at the CAS. The S-node moves from path $P, g, $ (inside the T-node) to path P,gP, g (directly in the branch gg), which is the same path, so (b) holds; jj stops being reachable, while its mainmain is a T-node, which is (e). CONTRACT then acts as above.
  • Line L28 does the same for the single I-node i′i', after it read a T-node in i′.maini'.main, which stays (Lemma 1).

No other step removes a branch that is an I-node, so (e) holds. □\square

Lemma 3 (the abstract map changes only at update points). A successful CAS of line L10, L13 or L15 sets the pair of s.keys.key to s.values.value in MM and changes nothing else. A successful CAS of line L21 or L25 removes the pair of kk from MM, and the pair removed is the one returned. Every other successful CAS (lines L26 and L28) leaves MM unchanged.

Proof. The CAS changes MM only below ii, which is reachable by Lemma 2(e). The cases are those of the proof of Lemma 2: lines L10, L13, L15 add or replace the S-node of the key; line L21 removes bb, whose key is kk, and returns b.valueb.value, with CONTRACT only regrouping the remaining S-node; line L25 removes the pair of kk from the L-node, whose value is returned. At lines L26 and L28, each S-node that moves is still reachable afterwards, and nothing else changes. □\square

Lemma 4 (reads). Let an operation on key kk read mm from i.maini.main at line L2 or L17, at depth dd, and let mm be a C-node or an L-node. At the moment of the read, ii is reachable with path hash(k)0,…,hash(k)d−1hash(k)_0, \ldots, hash(k)_{d-1}.

Proof. The root is reachable with the empty path. The operation reached ii through branches chosen by FLAGPOS on hash(k)hash(k), from C-nodes it read in their I-nodes. By induction on dd, its parent pp was reachable with the right path when the thread read the C-node mpm_p of pp, and ii was the branch of group hash(k)d−1hash(k)_{d-1} of mpm_p. So ii was reachable just then. Afterwards, ii stays reachable with the same path until its mainmain becomes a T-node (Lemma 2(e)). At the later read, i.main=mi.main = m is not a T-node, and by Lemma 1 it was not a T-node at any moment in between. So ii is still reachable with the same path. □\square

Theorem 1 (linearizability). Each history of the Ctrie is linearizable with respect to the sequential map.

Proof. Choose the points:

  • insert: its successful CAS at line L10, L13 or L15;
  • remove that returns a value: its successful CAS at line L21 or L25;
  • lookup, and remove that returns NOTFOUND: the last READ of an I-node at line L2 or L17 in the last round, the one that did not restart.

Each point is inside its call; the order of the points respects real time. By Lemma 3 the updates change MM exactly as the sequential operations do at their points, with the right results.

A lookup or a remove that returns NOTFOUND or a value without a CAS read mm from ii at depth dd, with ii reachable on the path of kk by Lemma 4. By Lemma 2(b) and (c), the S-node of kk, if any, is under ii, in the branch of group hash(k)dhash(k)_d of mm, or in mm if mm is an L-node. The node mm is immutable. If the group is empty, k∉Mk \notin M at that moment. If the branch is an S-node, it decides: its value, if its key is kk, or NOTFOUND, since kk can be nowhere else. If the branch is an I-node, the read was not the last one. In an L-node, the value of kk in it decides. So the result is M(k)M(k), or NOTFOUND, at the point. Rounds that restart do not change MM except through CLEAN, which changes nothing by Lemma 3. □\square

Theorem 2 (lock-freedom). In every infinite execution in which some operation is pending, infinitely many operations finish.

Proof. Suppose that from some moment τ\tau no operation finishes. Then after τ\tau no CAS of an update point succeeds, since a call whose update CAS succeeds ends after its line L22 and CLEAN-PARENT, and CLEAN-PARENT goes back to line L27 only after a failed CAS on p.mainp.main, so only if some other CAS on p.mainp.main succeeded. So after τ\tau the only successful CASes are those of lines L26 and L28.

Count, for the reachable part of the trie, the potential Φ\Phi = the number of reachable I-nodes. Each successful CAS of line L28 removes one reachable I-node, and one of line L26 removes at least as many as it resurrects; a CAS of line L26 that resurrects nothing and changes nothing else replaces a C-node by an equal copy. After τ\tau no CAS adds a reachable I-node, since only line L13 creates them. So the successful CASes of lines L26 and L28 that remove an I-node are finitely many. A thread does a CAS at line L26 only after it found a T-node in an I-node jj below pp; if the CAS removes nothing, jj was not in mm, so jj had already been detached, and after its restart from the root the thread cannot meet jj again (Lemma 2(e)). There are finitely many detached I-nodes with T-nodes (each was reachable before τ\tau or has been detached since, and none is created). So after some moment τ′≥τ\tau' \ge \tau no CAS succeeds at all, and every reachable I-node has a fixed mainmain. From then on, a thread that restarts goes down a trie that does not change; it does not meet a T-node, since any reachable T-node would be met by a cleaning CAS that succeeds, contradicting τ′\tau'; so its next CAS, on the current node of a reachable I-node, succeeds, or it returns without one. Both contradict the assumption. ■\blacksquare

Back to Ctrie (concurrent hash-array mapped trie)