Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

lock-free / wait-free 数据结构

一句话

"lock-free" 这词被用滥了。很多人把它当成"无锁 = 快=好"的玄学。实际上:

  • lock-free:保证至少有一个线程在做进展 (system-wide progress);
  • wait-free:保证每个线程在有限步内完成 (per-thread progress);
  • 朴素互斥既非 lock-free 也非 wait-free;

工程上做 lock-free 的代价主要不是数据结构本身, 而是 memory reclamation (ABA / Hazard Pointer / epoch reclamation). 这一章把 Treiber Stack / Michael-Scott Queue / Hash Map 这些经典骨架拆到一次看完, 让你动手写不踩坑.

1. 朴素无锁栈 (Treiber Stack)

struct Node { Node *next; T value; };
atomic<Node*> top;

void push(T x) {
    Node *n = new Node{x, top.load(memory_order_relaxed)};
    while (!top.compare_exchange_weak(n->next, n,
                                       memory_order_release,
                                       memory_order_relaxed)) ;
    // CAS 失败: 重试 (n->next 被 top 新值刷新)
}

T pop() {
    Node *top_old;
    top_old = top.load(memory_order_acquire);
    while (top_old && !top.compare_exchange_weak(top_old, top_old->next,
                                                  memory_order_acquire,
                                                  memory_order_acquire)) ;
    if (!top_old) return {};
    T val = top_old->value;
    delete top_old;  // 这里有 ABA 隐患
    return val;
}

Lock-free: 任何一次成功 push/pop (system-wide progress). 但 ABA 风暴在 pop 中出现.

ABA: thread A pop 后立刻 delete N0, 又有 malloc reuse same address 给 N1 同址. 此时 thread B 还停在 CAS: 看到 top=N0 没变化, 跑 CAS top=N0→next=N0.next, CAS 成功 — 但 N0 已被回收 → 数据结构空链 break.

2. ABA 的几个修复

  • Tagged pointer: ptr + counter 16 字节, 16 字节 CAS. ARMv8 / x86 (cmpxchg16b)支持. Python / Rust 的 crossbeam_epoch 都支持;
  • Hazard pointer: per-thread 一组"正在用"指针, 在回收前检查所有 hazard 槽, 没人持有才回收;
  • Epoch-based reclamation: 每 thread 在 epoch, 全 thread 离 epoch N ⇒ 回收 epoch N 之前垃圾. crossbeam / Java / Linux RCU 都类 epoch 模型.

工程上 epoch-based 最快且代码量可控. Hazard pointer 简单但慢 (每 access 检查 hazard). Tagged 用 CAS-double 内存对齐要求严.

3. 无锁队列 (Michael-Scott Queue)

struct Q {
    atomic<Node*> head;
    atomic<Node*> tail;
};

void enqueue(T x) {
    Node *n = new Node{x, nullptr};
    Node *t;
    while (true) {
        t = tail.load(memory_order_acquire);
        Node *next = t->next.load(memory_order_acquire);
        if (t == tail.load(memory_order_relaxed)) {      // tail 没变
            if (next == nullptr) {
                if (t->next.compare_exchange_weak(next, n, release, relaxed)) {
                    // 关键 step 1 完成
                    break;
                }
            } else {
                // 别人 advance tail 一半, 帮 advance
                tail.compare_exchange_weak(t, next, relaxed, relaxed);
            }
        }
    }
    // 关键 step 2: 把 tail advance 到新 node
    tail.compare_exchange_strong(t, n, release, relaxed);
}

Michael-Scott Q 是经典无锁队列, 实现要点:

  • 两个 step 不是原子: tail 不一定立即 advance, 别的 thread 可帮忙 advance (这就是 "helping" 模式);
  • helping 对 lock-free 算法非常关键 (保证 progress).

4. 无锁 hash map (e.g. Folly AtomicHashMap)

开放地址 + atomic CAS:

find bucket by hash;
linear probe: 
   if slot empty → CAS empty → new entry;
   if slot key matches → return;
   if marked tombstone → continue;
直到找到空 slot insertion / 找到 key.
扩容呢?
  这就是难处:
    扩容需要新 array, 但同时旧 reader 仍在用;
    解决: 用两个 hashmaps同 时存在 (转 move 期间), reference count 决定 free 旧的;

实际 lock-free hash map 远没有 lock-free stack / queue 那么干净. Folly AtomicHashMap 只支持单写多读, 不真完全 concurrent. Java ConcurrentHashMap v8 是 segment 锁 + CAS, 不算严格 lock-free.

5. lock-free 数据结构 benchmark 实测

Lockfree 不一定比 mutex 快:

1 thread stack mutex / Treiber Stack  20M ops/s, 两者一样
2 thread contended mutex   3M ops/s,  Treiber 4M ops/s
8 thread contended mutex   500k ops/s Treiber 1.5M ops/s
32 thread contended mutex  50k ops/s  Treiber 400k ops/s

lockfree 在高 contended 略好(2-3×), 但 epoch reclamation 加入后实际只快 1.5-2×. 并发到极端时 (64 thread+) epoch reclamation 也变成为 bottleneck; 实际 CSP (e.g. Go channel) 并不一定输;

6. wait-free 是不是必须

wait-free: 严格 stronger. 任何 thread 在限步内完成 → 一般上在生产代码中几乎从不实现, 因为常数远大于 lock-free, 在 cache-friendly 算法里反而更慢.

实际工程: "lock-free + benchmark 比 wait-free 多数 5-10× 快, 实际业务上没人强求 wait-free". Linux kernel 与标准库都 lock-free 不 wait-free.

7. 性能杀手: ABA 中的 memory reclamation cost

实际 lock-free 工程成本句号是内存回收:

  • hazard pointer 每访问需要 atomic store + thread-shared visibility; 对 hot path 加 5-15ns;
  • epoch reclamation 每隔 ~200us 才回收; 长时间 running 后退推进 epoch, 需要所有 thread 上报 → borderline throughput spiral;
  • crossbeam epoch V8 弱内容 fast but requires Rust async 不来 adwait.

8.这章带走的东西

  • lock-free = 至少 one thread 进展; wait-free = 每 thread 限步进展;
  • ABA 是 lock-free 第一个 wall: tagged pointer / hazard pointer / epoch reclamation 三路修;
  • Michael-Scott Queue 的 helping pattern;
  • lock-free 不一定比 mutex 快: 高 contended 多核下 1.5-2×, 低 contended 反而慢;
  • 工程上 lock-free 复杂度集中在 memory reclamation, 不在数据结构本身.

下一节 → IO 与网络栈