不能直接用std::map加锁替代SkipList,因其红黑树写操作需全局锁导致高并发下锁竞争严重;SkipList通过分层和原子指针+版本号实现细粒度并发控制,配合hazard pointer等内存回收机制保障安全。

为什么不能直接给 std::map 加锁来替代 SkipList?
因为 std::map 是红黑树,单次操作时间复杂度是 O(log n),但所有写操作(插入/删除)都得串行获取同一把全局锁,吞吐量卡在锁竞争上——尤其在高并发随机写场景下,CPU 都在等锁,不是在干活。SkipList 天然分层,可以只锁局部链表节点,实现更细粒度的并发控制。
如何设计节点结构才能避免 ABA 问题和内存重用风险?
必须用原子指针 + 带版本号的 tag pointer(如 std::atomic<uint64_t></uint64_t> 存指针+2位版本),不能只用 std::atomic<node></node>。否则在 CAS 更新时,一个节点被删、内存回收、又被新节点复用,地址相同但语义不同,CAS 会误判成功。
- 每个节点的
next数组元素类型应为std::atomic<uint64_t></uint64_t>,高位存版本,低位存指针 - 插入前需用
compare_exchange_weak循环重试,每次失败后重新读取并校验版本 - 删除节点时不能立即
delete,要走 hazard pointer 或 epoch-based reclamation(如libcds的gc::HP)
层数(level)生成策略对并发性能影响有多大?
用 rand() % MAX_LEVEL 是错的——它破坏概率分布,导致高层节点过少,跳表退化成链表;而固定层数(如始终 4 层)又限制扩展性。正确做法是按概率 p=0.5 抛硬币:每上升一层,有 50% 概率停止。
- 必须用线程本地的
std::mt19937,不能共用全局rand(),否则产生争用 -
MAX_LEVEL建议设为 32,实际平均层数约 log₂(n),足够覆盖亿级节点 - 首次插入时若生成 level=1,后续插入可能让 head 节点的
next[1]为空,读线程需容忍空指针跳转
读操作真的能完全无锁吗?
可以,但前提是读线程不修改任何节点字段,且使用安全的指针加载方式。关键不是“不加锁”,而是“不阻塞”——靠原子读 + 版本校验保证一致性。
立即学习“C++免费学习笔记(深入)”;
- 遍历某一层时,用
load(std::memory_order_acquire)读next[i],确保看到该指针指向节点的最新写入 - 若读到某个节点后,其
next字段突然变成 null(被删),需回退到低层重试,不能直接 crash - 不支持「快照遍历」(即遍历时要求看到某一时刻全量状态),那是 MVCC 或 RCU 才解决的问题
真正难的不是写逻辑,是内存生命周期管理——没做 hazard pointer 或 epoch 回收,多线程跑几天必崩在野指针上。










