如何设计一个高性能可扩展的自旋锁

网友提问:
为什么 OB 不采用 内核提供的 spin_lock
() 自旋锁,而是用户态态自己定义,并且还定义好多类型
短期临界区:优先 ObSpinLock(轻量互斥)
高并发读:优先 ObQSyncLock(无死锁检测但高性能)
复杂控制需求:ObLatch(统计信息)、ObSpinRWLock(Guard 类)提供额外管理能力。
如何理解 ?用户态实现,无系统调用开销
为了帮助 快速理解下面内容 模拟对话的方式
小义:充当候选人老王:充当面试官核心内容:
原子操作:c++11 提供比较并交换(Compare and Swap,CAS) 和内存模型
锁冲突后处理策略。

老王:在工作中使用过自旋锁吗?谈谈你对自旋锁理解?
小义:自旋锁是一种基于忙等待的锁机制,条件不满足时候,一直循环占用 cpu
老王: 这完全是按照字面意思翻译 ,不像使用过的样子,使用场景是什么?小义:自旋锁 是高并发场景下,多线程(多核)同步机制,相比自旋锁,通过一个原子变量判断是否加锁?
老王:原子操作 只能修饰简单整数吗?通过++操作完成?还有呢 原理是什么?小义:在 c++11 中,原子操作是一个模版 类 struct atomic<U*> ,
提供写操作 store,
读操作 load
比较并交换(Compare and Swap,CAS) compare_exchange
老王:c++内存模型一共 5 个方式,memory_order_acquire 是什么含义?
小义: 每个变量都存在一个虚拟地址,其中每个程序员都应该知道的延迟数字 ,地址 在 L1 L2 缓存 也可能 L3 缓存,也可能在物理内存,甚至磁盘上,多线程读写同一个变量,不是通过(锁,条件变量方式)内核方式,通过指令方式保证读 写操作原子性:
例如:一个整数,操作:先读取(1),然后打印(2)这个代码顺序,设置 memory_order_acquire 后,保证代码(2) 不在 代码(1)之前执行,也就是说,步骤(1)读取最新数据,然后执行步骤 2.
如何保证步骤 1 正确读取,需要不同循环执行比较并交换(Compare and Swap,CAS)
老王:然后呢,还有吗?自旋锁 占用 cpu 比较高如何解决?小义:如果第一加锁不满足条件,可以尝试 sleep 方式,避免多 cpu 抢占
老王:sleep 不是放弃 cpu 了吗?这个退避算法不合理?小义:在自旋锁加锁冲突情况下,可以参考 folly::MicroSpinLock 和 Futex 机制,
✅ 特点
使用一个 字节(uint8**_**t) 实现的超轻量锁;
调用者会进行自旋(通常用
pause
指令减轻总线压力);如果没抢到,调用
sleeper.wait()
执行 指数退避(exponential backoff):一开始忙等自旋;
然后逐步用
pause()
;然后可能
std::this_thread::yield()
;【这个时候哈仔用户态】最后可能 sleep 一小会;
全部仍然在用户态完成,不进入内核。🎯 适用场景:
临界区 非常小(几十纳秒);
高并发 + 短冲突窗口;
场景:如锁粒度极细的统计数据、metrics 缓存更
2️⃣ Futex(Fast Userspace Mutex)
这是 Linux 提供的用户态锁机制,用来在锁竞争严重时自动切换到内核阻塞。
✅ 特点:
通常实现为:
尝试快速在用户态获取锁;
如果失败,调用
futex_wait()
→ 内核挂起当前线程;解锁时调用
futex_wake()
→ 唤醒阻塞线程;是 pthread**_**mutex, std::mutex 等的底层实现方式;
老王:还是每个线程都在抢占 锁这个变量 还有其他方式吗?
小义:还有一个方式 那就是 银行排队排号办理业务情况,依然是在用户态原子比较判断,但是定义 2 个变量
老王:在 NUMA 系统中,访问跨节点共享数据时,自旋锁导致 cache 频繁失效,性能下降。
小义:Liunx4.2 起默认使用 qspinlock,每个 cpu 读取各种缓冲区变量,避免 cache 失效
老王:能详细说说吗?多个 thread 对 spinlock 的读写造成的 cache bouncing 问题,我们引入了[per cpu]的 mcs lock,让 thread 自旋在各自 CPU 的 mcs lock,从而减少了缓存颠簸问题,我不理解
来源:# Linux 中的 spinlock 机制[三] - qspinlock
小义:类比 ThreadLocal,这个我没研究过,如果其他了解的 可以告诉我一下 Qspinlocks
老王:换个思路 单 cpu,内核状态自旋锁 需要禁用 中断吗?用户态为什么不需要?
小义:用户态不涉及中断上下文切换。cpu 有特权指令,同时面临竞争操作。
ob 代码中不同自旋锁实现方式 【可跳过不看】
ObSpinLock 与传统自旋锁的区别主要体现在以下几个方面:
实现机制不同:
传统自旋锁:直接基于 CPU 原子指令(如 CAS)实现,线程持续轮询检查锁状态
ObSpinLock:基于 ObLatchMutex 实现,是一个更高级的抽象,不是直接使用原子操作循环
等待策略不同:
传统自旋锁:一直忙等(busy-wait),持续占用 CPU 资源
ObSpinLock:采用混合等待策略,先自旋一定次数,若仍未获取到锁则主动让出 CPU,避免 CPU 资源浪费
可重入性检查:
传统自旋锁:通常不支持自检是否可重入
ObSpinLock:提供 self_locked()方法,可以检查锁是否被当前线程持有
调试能力:
传统自旋锁:调试困难,难以确定谁持有锁
ObSpinLock:可通过 get_wid()获取持有锁的线程 ID,便于调试和分析
代码 实现:ObSpinLock--->ObLatchMutex ---int ObLatchMutex::lock()
主要分析:加锁失败后怎么处理的
ObLatchMutex::lock 中加锁失败后的处理逻辑主要分为三个层级的策略:
自旋重试(Spinning):
首先通过 low_try_lock 尝试以原子方式获取锁
如果获取失败,会在一个紧凑的循环中多次尝试,直到达到最大自旋次数
这个阶段线程处于活跃状态,持续消耗 CPU 资源,适合短暂的锁竞争情
主动让出 CPU(Yielding): else if (yield_cnt < OB_LATCHES[latch_id].max_yield_cnt_) { sched_yield(); ++yield_cnt; continue; }
线程挂起等待(Sleeping)
调用 wait 方法将线程挂起,完全释放 CPU 资源
在 wait 方法中,通过 futex 机制实现高效的线程挂起
关键设计思想:
梯度递进的等待策略:从积极(自旋)到保守(挂起)递进,平衡响应速度与资源消耗
效率优先:短期自旋避免线程调度开销,适合短临界区
资源友好:长期等待时主动让出 CPU 或挂起,避免 CPU 浪费 sched_yield
超时机制:支持等待超时,避免无限阻塞
系统调用优化:使用高效的 futex 机制减少系统调用开
ObQSyncLock 怎么体现高并发
Sync 机制的优势:
使用了单独的 write_flag_成员变量,与读计数分开,减少了资源争用
基于 ObQSyncWrapper<MAX_REF_CNT>实现,这是一种专门为高并发读优化的机制
读操作在无写入时几乎无锁化,只需更新计数器
支持高并发度(MAX_REF_CNT=48),可同时允许最多 48 个并发读操作
读操作:
GCC 提供的 atomic 非 c++提供高级抽象
性能优化关键点:分散的计数器管理
每个线程使用固定的计数器槽位:idx = get_id() % ref_num_
这大幅减少了缓存争用,特别是在多核系统上
缓存友好设计 int64_t ref_ CACHE_ALIGNED; // 确保每个计数器独占缓存行
防止伪共享(false sharing)导致的性能下
为什么这对 ObQSyncLock 至关重要
ObQSyncLock 的核心设计是将读锁引用计数分散到多个槽位:
高频原子操作:
每次读锁获取和释放都涉及对特定 ref_的原子增减操作
如果没有缓存行对齐,这些高频原子操作会导致严重的伪共享问题
吞吐量影响:
在读多写少的高并发场景中,不同线程可能同时尝试获取读锁
如果多个 ref_共享缓存行,性能可能下降 5-10 倍甚至更多
按线程 ID 分配:
ObQSyncLock 将线程固定映射到特定的 ref_槽位:idx = get_id() % ref_num_
这与 CACHE_ALIGNED 配合,确保同一线程总是访问同一缓存行
大幅提高了缓存命中率
还是不明白?
总结
多核情况下 如何解决 Cache Line 不命中问题,ob 和 内核采取类似方式。cpu/线程读取本地的数据。
具体怎么实现的没看明白
缓存伪共享是多核处理器系统中的一个常见性能瓶颈:
原理:
现代 CPU 的缓存以缓存行(Cache Line)为单位组织,通常是 64 字节
当多个变量共享同一个缓存行时,任何一个变量被修改都会导致整个缓存行失效
这会导致其他 CPU 核心必须重新从内存加载整个缓存行,即使它们只访问缓存行中未修改的变量
影响:
在多线程环境中,如果多个线程频繁修改位于同一缓存行的不同变量
会导致缓存行在各 CPU 核心之间不断"乒乓"传递
极大降低缓存效率,增加内存访问延迟,导致性能下降
CACHE_ALIGNED 宏的作用
CACHE_ALIGNED 是一个编译器指令,用于确保变量按缓存行大小对齐
struct Ref {Ref(): ref_(0) {}~Ref() {}int64_t ref_ CACHE_ALIGNED; // 确保每个计数器独占缓存行};
我是谁
最动人的作品,为自己而写,刚刚好打动别人*刚刚好,是最难得的美好
这里的节奏刚刚好,不必焦虑,自有充实与希望;
这里的人情味儿刚刚好,不必刻意,
到处都是真情与温暖;
我在这里,我刚刚好。

评论