不能直接用std::shared_mutex给整个skiplist加锁,因为粒度太大,会阻塞所有读操作,使并发吞吐退化至单线程水平;应采用分层细粒度锁,仅锁定实际修改的层级列,并严格按从高到低顺序加锁以避免死锁。

为什么不能直接用 std::shared_mutex 给整个 SkipList 加锁
因为读写锁粒度太大,会把并发优势全抵消掉——插入或删除一个节点时,所有读操作都得排队等,吞吐直接掉到单线程水平。SkipList 的层级结构天然支持分段加锁,关键在于:**只锁涉及修改的那几列(column),而不是整条链**。
常见错误是给 head 节点加一把大锁,结果 find() 和 insert() 在同一层反复争抢,压根没发挥跳表“多层索引并行查找”的优势。
- 真正要保护的是每个节点的
next指针数组,尤其是被修改的那一层 - 读操作(如
contains())只要不修改指针,多数路径可完全无锁——靠原子读 + 内存序保证可见性 - 写操作必须对从
head到目标节点路径上、每一层的前驱节点加锁(按层从高到低顺序获取,避免死锁)
怎么给每层节点设计细粒度锁
别用 std::mutex 塞进节点里——内存开销爆炸,且锁数量随层数动态变化。更实用的做法是:**用数组式分段锁(shard lock)映射到 level 层**。
例如固定最高 16 层,预分配 std::array<:shared_mutex> level_locks</:shared_mutex>;插入时,对实际用到的层数(比如 0~3 层)依次加 shared_lock(读)或 unique_lock(写)。
- 查找时对每层只持
shared_lock,多个读可并行 - 插入/删除时,先升序获取所有涉及层的
unique_lock,再自顶向下遍历并更新指针 - 注意:锁的顺序必须严格从高层(level max)向低层(level 0)进行,否则可能死锁
- 不要在锁内做耗时操作(如内存分配、随机数生成),
randomLevel()应提前算好
原子操作在哪用、怎么用才安全
纯读场景(如 contains())可以全程无锁,但必须用 std::atomic_load(&node->next[i], std::memory_order_acquire) 替代普通指针解引用;否则编译器或 CPU 可能重排指令,读到中间态指针(比如新节点还没完全连好就去访问了)。
组合式C++代码评审方案,融合静态分析、AI推理、多轮迭代评审和C++专项检查,适用于PR审查、增量代码审查、全项目评审和代码质量评分,触发词包括review cpp、cpp代码评审、C++review、代码审查。
写操作中,仅靠原子操作不够——它不能保证多步操作的原子性(如“找到前驱→修改 next→更新后继”这三步)。所以必须组合:**原子读用于遍历,互斥锁用于修改临界链路**。
-
next指针字段必须声明为std::atomic<node></node>,初始化为nullptr - 写入时用
store(ptr, std::memory_order_release),确保之前所有修改对其他线程可见 - 禁止对原子指针做
++或+=,跳表不支持指针算术 - 如果用
std::atomic_flag实现自旋锁,注意避免在高竞争下饿死,建议 fallback 到系统 mutex
插入失败时怎么避免 ABA 问题
当两个线程同时尝试插入同一 key,在 CAS 更新前驱节点的 next[level] 时,可能第一个线程已成功插入并删除,第二个线程看到指针值没变就误判成功——这就是 ABA。跳表中尤其危险,因为节点频繁复用(比如内存池回收)。
标准解法是给指针配上版本号(tagged pointer),但 C++20 前没原生支持。更务实的做法:**不用 CAS 做插入主逻辑,改用锁+双重检查**。
- 先用共享锁快速遍历,确认 key 不存在
- 升级为独占锁,再次确认(防止期间被插入)
- 确认无冲突后再分配节点、拼接指针
- 不依赖 CAS 插入,自然避开 ABA;代价是少量重复遍历,但比 ABA 导致数据错乱好得多
真正难处理的是删除后的内存回收时机——用 hazard pointer 或 epoch-based reclamation 才能安全释放节点,这部分一旦出错会导致 use-after-free,调试极困难。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!










