跳表多线程下不能仅用std::shared_mutex,因其写操作需跨层级修改且路径不固定;应采用分段锁+无锁读,每段配mutex,读用原子指针与acquire内存序。

为什么跳表在多线程下不能直接加锁 std::shared_mutex 就完事?
因为跳表的写操作(插入/删除)会修改多个层级的指针,且路径不固定——你锁住某个节点,不代表它前驱或后继节点没被其他线程同时修改。用 std::shared_mutex 全局读写锁会导致读吞吐骤降;只锁单个节点又无法保证结构一致性。
真正可行的路是「分段锁 + 无锁读」:把跳表按 key 范围切分成若干段(比如 64 段),每段配一把 std::mutex,写操作只锁涉及的段;读操作则全程无锁,靠原子指针和内存序保障可见性。
- 段数太少 → 锁竞争高;太多 → cache line false sharing 和管理开销上升
- key 分段不能用取模(
hash(key) % N),得用高位截断(如(uint64_t)key >> 48),避免哈希碰撞集中 - 读操作必须用
std::memory_order_acquire加载指针,否则可能看到未完成的中间状态
std::atomic 指针怎么安全地更新跳表层级指针?
跳表每个节点的 next 是个指针数组,但 C++ 不允许对数组做原子操作。常见错误是把整个 next[] 声明为 std::atomic<node></node> 数组——这不合法,编译不过。
正确做法是:每个层级的指针单独声明为 std::atomic<node></node>,例如 std::atomic<node> next[LEVEL_MAX]</node>。更新时逐层 CAS(Compare-And-Swap),并确保高层更新前,低层已稳定。
立即学习“C++免费学习笔记(深入)”;
- CAS 失败不等于失败:可能是别人刚改了同一层,需重试;但若发现某低层指针变为空,说明节点正被删除,应中止当前插入
- 层级高度必须在插入前就确定(如通过随机数),不能边走边生成,否则无法保证多线程下结构可预测
- 所有
std::atomic操作必须显式指定内存序:load()用acquire,store()用release,compare_exchange_weak()默认是acq_rel
自适应层级(adaptive level)到底该响应什么信号?
所谓“自适应”,不是指运行时动态调整整个跳表的 LEVEL_MAX,而是针对每个新插入节点,根据当前负载实时决定其高度。关键信号只有两个:size() 和最近一次 GC 的碎片率。
别被论文带偏——不需要复杂反馈控制。实践中有效策略是:当总节点数超过 1 且最近删除密度 > 30%,就将新节点初始高度 +1(上限仍为 <code>LEVEL_MAX)。
- 不要用系统时间或线程 ID 做随机源,
thread_local std::mt19937更可靠 - 高度提升是“倾向性”而非强制:仍以概率
1/4决定是否升层,只是基础概率从0.5调到0.75 - 层级过高(>32)反而降低缓存局部性,
LEVEL_MAX设为 24 是 x86-64 下实测较优值
并发读写下如何避免 ABA 问题导致的节点悬挂?
跳表删除节点时,若仅靠 CAS 把前驱的 next[i] 从 A 改成 B,而 A 被释放后内存又被分配给新节点 A',下次 CAS 可能误判成功——这就是 ABA。C++ 标准库的 std::atomic<t>::compare_exchange_weak</t> 本身不防 ABA。
解决方案不是引入 std::atomic<uintptr_t></uintptr_t> 手动拼地址+版本号(太重),而是复用节点内存池 + hazard pointer。每个线程维护一个 hazard_ptr 数组,读操作开始前把正在访问的节点指针存进去;删除线程只回收那些没被任何 hazard_ptr 引用的节点。
- hazard pointer 数组大小设为 4 即可覆盖 99% 场景,再多是浪费
- 必须配合惰性回收:删除标记后不立即 free,等所有线程都完成一次 quiescent state(比如遍历完一次跳表)再批量回收
- 别忘了在析构函数里调用
hazard_thread_retire(),否则进程退出时内存泄漏
最易被忽略的一点:hazard pointer 的存储位置必须是 thread_local,且初始化要早于任何跳表操作——晚了会导致首次读就 crash。











