绝大多数场景下应优先使用 std::map 或 absl::flat_hash_map,而非手写 lock-free skiplist;仅当高频混杂操作、零锁竞争刚需且性能瓶颈确凿时才考虑,因其涉及原子顺序、内存回收、ABA 防御、层级生成等多重高危难点。

为什么别急着手写 lock-free skiplist
绝大多数场景下,std::map 或 absl::flat_hash_map 比自己搞 lock-free skiplist 更快、更稳、更省心。真正需要它的,通常是高频插入/查找/删除混杂 + 无法接受锁竞争(比如内核旁路网络栈、超低延迟交易中间件),且 profiler 明确指出红黑树或哈希表成了瓶颈。
手写 lock-free skiplist 不是“加个 std::atomic 就完事”,它要同时处理:多层指针的原子更新顺序、内存回收(ABA 问题)、节点生命周期管理、层级随机生成的公平性。一个没压住,就出现静默数据错乱或无限循环。
- 常见错误现象:
find()返回已删除节点、insert()卡死在compare_exchange_weak循环、delete()后find()偶尔返回nullptr却又读到脏值 - 关键难点不在“跳”,而在“删”——如何安全释放节点内存?用 hazard pointer 还是 epoch-based reclamation?选错直接导致 use-after-free
- 层级生成不能用
rand():多线程下若共享staticseed,会坍缩成单链表;必须每个线程用独立std::random_device+std::mt19937
skiplist 节点怎么定义才不踩 ABA 坑
核心是把“逻辑删除”和“物理删除”彻底分开。节点里不能只存 next 指针,得拆成两层标记:
-
next字段必须是std::atomic<node></node>,且所有修改都走compare_exchange_weak - 加一个
marked标志位(可复用指针最低位,或单独std::atomic<bool></bool>),表示该节点已被逻辑删除(即后续insert遇到它要跳过) - 绝对不要在
delete时直接delete node—— 必须等所有线程确认不再引用它,再进内存回收队列
示例片段(简化):
立即学习“C++免费学习笔记(深入)”;
struct Node {
int key;
std::atomic<Node*> next[LEVEL_MAX];
std::atomic<bool> marked{false};
};
注意:marked 和 next[i] 的更新顺序必须严格:先置 marked = true,再 CAS 断开前驱的 next[i],否则其他线程可能在 marked 还没设时就跳过该节点,导致漏删。
insert / delete 的 CAS 循环怎么写才不活锁
lock-free 算法本质是“乐观重试”,但 naive 的 while 循环容易让线程饿死。关键在两点:定位路径缓存 + 退避策略。
- 每次
insert或delete前,必须从 head 开始完整遍历并记录“搜索路径”(每层最后未越界的节点指针),不能边走边改——否则路径中途被其他线程改掉,CAS 就失效 - CAS 失败后不能立即重试:加
std::this_thread::yield()或短延时(std::chrono::nanoseconds(1)),避免 CPU 空转抢资源 - 层级更新必须自上而下:先确保第
level-1层连上,再改第level-2层,否则低层已连但高层断开,会导致查找失败
典型错误:insert() 中对某层 pred->next[i] CAS 失败后,没重走整个搜索路径,而是直接用旧 pred 继续试下一层——pred 可能早已被删,next[i] 是悬垂指针。
用现成库比手写靠谱得多
如果你不是在写论文或打磨底层基础设施,直接用 libcds 的 cds::container::SkipListMap 或 Folly 的 folly::Synchronized + 读写锁版 skiplist。它们经过多年生产验证,覆盖了 x86/ARM 内存序、TSO/RCpc 语义、各种编译器优化陷阱。
自己实现时最容易被忽略的,是编译器重排——哪怕用了 std::memory_order_acq_rel,GCC/Clang 仍可能把非原子变量访问挪到原子操作之前。必须用 std::atomic_thread_fence 显式约束,且 fence 类型要和你的 CAS 顺序匹配。
真要动手,先跑通 libcds 的单元测试,再逐行对照它的 SkipListMap 实现。别信网上那些“100 行搞定 lock-free skiplist”的博客——它们要么删了内存回收,要么只支持单线程删除,要么根本没测过 4 线程以上压测。


















