We finish the tour with the structure you reach for most β the hash table β and the lessons the whole chapter was really about.
29.4 Concurrent Hash Table
The trick is to not invent anything new. Build the hash table out of the concurrent lists you already have, one per bucket β and give each bucket its own lock rather than sharing one global lock:
#define BUCKETS (101)
typedef struct __hash_t {
list_t lists[BUCKETS]; // each bucket is a concurrent list
} hash_t;
void Hash_Init(hash_t *H) {
for (int i = 0; i < BUCKETS; i++)
List_Init(&H->lists[i]); // ... each with its OWN lock
}
int Hash_Insert(hash_t *H, int key) {
return List_Insert(&H->lists[key % BUCKETS], key);
}
int Hash_Lookup(hash_t *H, int key) {
return List_Lookup(&H->lists[key % BUCKETS], key);
}
(This one doesnβt resize β handling growth is left as an exercise.) Because a lock protects only one bucket, operations on different buckets never contend. Step through two concurrent inserts:
1The structure: one lock PER bucket
The whole table is 101 buckets, each a concurrent list from Β§29.2 with its own lock. Hash_Insert(key) simply calls List_Insert on bucket key % 101 β no global lock anywhere.
The payoff is dramatic. Compared with a single-lock list under the same four-thread insert load, the per-bucket hash table barely moves off the x-axis:
Figure 29.11: Scaling hash tables. Four threads, 10kβ50k concurrent inserts each. The single-lock list climbs steeply; the per-bucket hash table stays flat β it scales magnificently. (Empirical β measured on the authorsβ 4-CPU iMac.)
29.5 Summary
Four structures β counter, list, queue, hash table β and a handful of lessons that matter more than any one of them:
| why it matters | |
|---|---|
| Guard lock/unlock around control flow | error paths that return mid-function must undo every lock and allocation first |
| More concurrency β more performance | a scheme drowning in lock traffic can be slower than one big lock |
| Fix performance only once it exists | avoid premature optimization β no value in speeding up what was never the bottleneck |
Tip: Avoid Premature Optimization (Knuthβs Law)
When building a concurrent structure, start with the most basic approach β one big lock for synchronized access. Youβll likely get a correct lock; only if it then shows performance problems should you refine it. As Knuth famously put it, βpremature optimization is the root of all evil.βHistory bears this out. Early multiprocessor kernels used a single lock β in Linux it even had a name, the big kernel lock big kernel lock The BKL: a single coarse-grained lock guarding the entire kernel, used by early multiprocessor SunOS and Linux. A scaling bottleneck once multi-CPU machines became the norm β Linux replaced it with many fine-grained locks; Sun instead built Solaris for concurrency from day one. defined in ch. 29 β open in glossary (BKL). For years that simple approach was fine; only when multi-CPU systems became the norm did one-thread-in-the-kernel turn into a bottleneck. Linux took the straightforward path β replace one lock with many; Sun took the radical one β build a whole new OS, Solaris, with concurrency baked in from day one.
And this is only the surface. Beyond locks entirely lie non-blocking data structures, which coordinate with atomic instructions (like the compare-and-swap of chapter 28) instead of locks β a taste comes with common concurrency bugs, a couple of chapters on. An entire field waits there for the curious.
Check yourself: hash tables and lessons
1.Why does the concurrent hash table scale magnificently while the single-lock list does not?
2.With BUCKETS = 101, a thread calls Hash_Insert(104). What happens?
3.Following Knuth's law, how should you start when building a concurrent data structure?
4.What was the 'big kernel lock' (BKL), and why did Linux move away from it?
5.How do non-blocking data structures coordinate threads without traditional locks?