Proofs: Concurrent hash table
These are the human proofs of Concurrent hash table, at full length.
The striped hash table is linearizable and deadlock-free full
This proof shows: correctness, linearizability, progress.
We use the pseudocode of the page, with REMOVE as GET plus a deletion under the same lock. Write for the stripe of key , and for at a given moment. The map of the table at a moment is the set of pairs stored in the buckets of the current array.
Lemma 1 (stripe fact). At each moment divides . For each key and each bucket index , .
Proof. The constructor requires , and RESIZE replaces by , so always. If , then for some integer , and , so .
Lemma 2 (who changes what). (a) is written only at line L16, by a thread that holds all locks. (b) Bucket of the current array is changed only by a thread that holds lock or holds all locks. (c) is written only by a holder of lock .
Proof. (a) Line L16 is the only write of the field, and it lies between lines L13 and L17. (b) PUT and REMOVE change bucket of the array they read at line L2 (or its analogue), while they hold lock . They read the array while holding that lock, and by (a) the array cannot be replaced while any lock is held by them, so the bucket they change is in the current array; by Lemma 1 its index has . RESIZE fills a new array before line L16 publishes it, while it holds all locks. (c) The counter is changed at line L5 and in REMOVE under lock .
Lemma 3 (invariant). Whenever no thread holds a lock, invariant (S) holds: each pair is in bucket , no key is in two pairs, and is the number of pairs with . Moreover, at each moment, the pairs with keys of stripe are changed only by the current holder of lock .
Proof. The second claim is Lemma 2 (b) with Lemma 1. For (S), we show that each critical section that starts in a state where (S) holds for its stripe leaves it holding. A PUT holds lock and looks only at bucket . All pairs with key would be in that bucket by (S), so the search of line L3 finds the pair of if there is one. Line L4 replaces its value, and line L5 appends a pair with a new key to the right bucket and adds one to the right counter. So (S) holds after it. REMOVE is the same with a deletion. GET changes nothing. RESIZE holds all locks, so by Lemma 2 no other thread changes any bucket or counter while it runs; line L15 moves each pair to bucket of the new array, which keeps the keys unique and the counters right, and line L16 makes the capacity. The map does not change.
Theorem (linearizability). Every history of the table is linearizable with respect to the sequential dictionary.
Proof. Let the linearization point of each PUT, GET and REMOVE be the step at which it completes its acquire of lock (line L1 or line L10). This step is inside the call. A pending call that has not taken its lock has changed nothing; remove it. A pending call that holds its lock may have changed its bucket; it holds the lock for ever in this history, so no other call on its stripe takes a later point. Keep it if it has made its change (complete it with the result it computed), else remove it. A RESIZE inside a PUT does not change the map (Lemma 3), so it needs no point.
Order the kept calls by their points into the sequential history . If call returns before call starts, the point of comes first, so keeps real time. It remains to show that each call has in the result it has in the history.
Take a call on key , with point . In the sequential dictionary, the result of depends only on the earlier calls on the same key : a put, get or remove on key never changes the value of . The calls on key all take the same lock , so their critical sections are disjoint and ordered as their points. So each call on key with a point before has finished its critical section before takes the lock, and each call on with a later point has not yet started its critical section. By the second claim of Lemma 3, the only changes to the pairs of stripe while holds the lock are its own, and RESIZE does not change the map. So sees, for key , exactly the value that the calls on before produce in , and it returns what the sequential dictionary returns at that place of .
Theorem (deadlock-freedom). If each lock is deadlock-free, then whenever calls are pending and the threads keep taking steps, some call returns.
Proof. Outside RESIZE a thread holds at most one lock, and it waits for no other lock while it holds it. Inside RESIZE a thread waits for lock only while it holds locks . So each thread that holds locks and waits waits for a lock with a larger index than each lock it holds. A cycle of waits , where waits for a lock held by , would then need an index that increases strictly around the cycle, which is impossible. Each critical section is finite: a PUT, GET or REMOVE scans one finite chain, and a RESIZE moves the finite set of pairs. So a lock holder that takes steps releases its locks, and some waiter then enters by deadlock-freedom of the lock, and finishes.
A RESIZE may run twice for one doubling only if line L14 were absent: the second caller passes the capacity it saw, finds the capacity , and does nothing. So each doubling happens once per distinct full capacity.