为什么OB不采用 内核提供的 spin_lock
() 自旋锁, 而是用户态态自己定义,并且还定义好多类型
如何理解 ?用户态实现,无系统调用开销
小义:充当候选人 老王:充当面试官 核心内容:
自旋锁发展
Linux 版本 | 时间 | 主要进展 | 自旋锁相关说明 |
---|---|---|---|
2.0 | 1996 | 支持 SMP,BKL 引入 | 全局大内核锁,串行化所有内核操作,无需细粒度锁 |
2.2 | 1999 | 引入基础自旋锁机制 | 初步在某些子系统中使用 spinlock_t,开始去BKL化 |
2.4 | 2001 | 更广泛使用自旋锁 | 自旋锁与 cli/sti(关中断)结合使用,锁粒度更细 |
2.6 | 2003 | 高度并发与抢占内核 | 引入 spin_lock_irqsave() 等多种锁版本,应对复杂场景 |
3.x | 2011 | BKL 基本移除 | 通过细粒度自旋锁和 mutex 替代 BKL |
4.2 | 2015 | 引入 qspinlock | MCS 队列自旋锁替代原始的 ticket lock,提高扩展性 |
5.x | 2018+ | NUMA、多核优化 | 更复杂的锁策略:退避、统计锁竞争、结合 RCU 等 |
老王:在工作中使用过自旋锁吗?谈谈你对自旋锁理解?
小义:自旋锁是一种基于忙等待的锁机制,条件不满足时候,一直循环占用cpu
老王: 这完全是按照字面意思翻译 ,不像使用过的样子,使用场景是什么? 小义:自旋锁 是高并发场景下,多线程(多核)同步机制,相比自旋锁,通过一个原子变量判断是否加锁?
老王:原子操作 只能修饰简单整数吗?通过++操作完成?还有呢 原理是什么? 小义:在c++11中,原子操作是一个模版 类 struct atomic<U*> ,
老王:c++内存模型一共5个方式,memory_order_acquire是什么含义?
序 | 语义保证 | 开销 |
---|---|---|
memory_order_relaxed | 仅保证原子性,不保证与其他操作的顺序或可见性 | 最低 |
memory_order_acquire | 保证此操作后续的读写不会在指令序上移到此操作之前 | 较低 |
memory_order_release | 保证此操作前的读写不会在指令序上移到此操作之后 | 较低 |
memory_order_acq_rel | 同时具备 acquire 和 release 语义,对应读写双向屏障 | 中等 |
memory_order_seq_cst | 全局顺序一致性;在所有线程中可见操作顺序完全一致 | 最高 |
小义: 每个变量都存在一个虚拟地址,其中 每个程序员都应该知道的延迟数字 ,地址 在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机制,
✅ 特点
pause
指令减轻总线压力);sleeper.wait()
执行 指数退避(exponential backoff):pause()
;std::this_thread::yield()
;【这个时候哈仔用户态】2️⃣ Futex(Fast Userspace Mutex)
这是 Linux 提供的用户态锁机制,用来在锁竞争严重时自动切换到内核阻塞。
✅ 特点:
futex_wait()
→ 内核挂起当前线程;futex_wake()
→ 唤醒阻塞线程;https://github.com/facebook/folly/blob/main/folly/SpinLock.h
SpinLock--->folly::MicroSpinLock
/*
* folly::MicroSpinLock
* ====================
* A really, *really* small spinlock for fine-grained locking of lots
* of teeny-tiny data.
*
* Designed to be POD (Plain Old Data) so it can be
* packed into other structures without extra overhead.
*
* 特性:
* 1. 极小自旋锁,适用于超细粒度场景
* 2. POD 类型,可零初始化,相当于调用 init()
* 3. 用户态实现,无系统调用开销
* 4. 支持动态检测工具插桩(ThreadSanitizer 等)
*/
struct MicroSpinLock {
// FREE 表示锁可用,LOCKED 表示锁已被持有
enum { FREE = 0, LOCKED = 1 };
//
// 用一个字节存储锁状态,避免使用 std::atomic<>,保持 POD 特性
//为什么用uint8_t表示 std::atomic<>?
//这个和序列化有什么关系?怎么实现的
typedef unsigned char uint8_t;
uint8_t lock_; //locked?
/**
* 尝试获取锁,非阻塞
* @return true: 获得锁;false: 未获得
*/
bool try_lock() noexcept {
// 调用 xchg 将新值置为 LOCKED,返回旧值
bool ret = (xchg(LOCKED) == FREE);
// 插桩:记录尝试获取锁的结果,便于动态分析
annotate_rwlock_try_acquired(
this, annotate_rwlock_level::wrlock, ret, __FILE__, __LINE__);
return ret;
}
/**
* 获取锁,阻塞自旋
* 在持锁竞争时,会进行退避等待以降低总线抖动
*/
void lock() noexcept {
detail::Sleeper sleeper;
// 自旋尝试交换,如果旧值不为 FREE,表示竞争失败
while (xchg(LOCKED) != FREE) {
// 进入退避等待循环
do {
sleeper.wait(); // 调用 pause/yield 或指数退避
} while (payload()->load(std::memory_order_relaxed) == LOCKED);
}
// 加锁成功,断言当前状态为 LOCKED
assert(payload()->load() == LOCKED);
// 插桩:记录获取锁的事件
annotate_rwlock_acquired(
this, annotate_rwlock_level::wrlock, __FILE__, __LINE__);
}
/**
* 释放锁
* 将状态设为 FREE,并使用 release 语义保证可见性
*/
void unlock() noexcept {
// 断言当前持锁状态
assert(payload()->load() == LOCKED);
// 释放锁 write
payload()->store(FREE, std::memory_order_release);
}
private:
/**
* 内部:获取指向 lock_ 字段的原子指针
*/
std::atomic<uint8_t>* payload() noexcept {
// reinterpret_cast 到 std::atomic<uint8_t>* 类型
return reinterpret_cast<std::atomic<uint8_t>*>(&this->lock_);
}
/**
* 原子交换操作,将 lock_ 原子地置为 newVal,返回旧值
*/
uint8_t xchg(uint8_t newVal) noexcept {
return std::atomic_exchange_explicit(
payload(), newVal, std::memory_order_acq_rel);
} //A read-modify-write operation with this memory order is both an _acquire operation_ and a _release operation_
};
老王:还是每个线程都在抢占 锁这个变量 还有其他方式吗?
小义:还有一个方式 那就是 银行排队排号办理业务情况,依然是在用户态原子比较判断,但是定义2个变量
参考:https://mfukar.github.io/2017/09/08/ticketspinlock.htm
now_serving next_ticket
| |
V V
... 0 1 2 3 4 5 6 7 0 1 2 ...
\___________/
8 threads
holding one ticket each
struct TicketSpinLock {
/**
* Attempt to grab the lock:
* 1. Get a ticket number
* 2. Wait for it
*/
void enter() {
/* We don't care about a specific ordering with other threads,
* as long as the increment of the `next_ticket` counter happens atomically.
* Therefore, std::memory_order_relaxed.
*/
const auto ticket = next_ticket.fetch_add(1, std::memory_order_relaxed);
while (now_serving.load(std::memory_order_acquire) != ticket) {
spin_wait();
} //没等到我 我就是wait
}
}
老王:在 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 有特权指令,同时面临竞争操作。
实现机制不同:
等待策略不同:
可重入性检查:
调试能力:
代码 实现:ObSpinLock--->ObLatchMutex ---int ObLatchMutex::lock()
主要分析:加锁失败后怎么处理的
ObLatchMutex::lock中加锁失败后的处理逻辑主要分为三个层级的策略:
线程挂起等待(Sleeping)
关键设计思想:
Sync机制的优势:
读操作:
int ObQSyncLock::rdlock()
{
int ret = common::OB_SUCCESS;
do {
if (common::OB_EAGAIN == (ret = try_rdlock())) {
sched_yield();
//sched_yield()会主动放弃当前CPU给其他进程使用;
//但是如果当前CPU上无其他进程等待执行,则直接返回继续执行当前进程
// 退避策略:使用sched_yield()让出CPU,而不是无限自旋
}
} while (common::OB_EAGAIN == ret); //读锁加锁失败,重试
return ret;
}
int ObQSyncLock::try_rdlock()
{
int ret = OB_SUCCESS;
if (OB_UNLIKELY(0 != ATOMIC_LOAD(&write_flag_))) {
ret = OB_EAGAIN;
//快速检查写标志:首先使用ATOMIC_LOAD原子操作检查write_flag_是否为0,
//如果不为0表示有写锁存在,直接返回OB_EAGAI
} else {
// 找到线程对应的引用槽位并增加计数
const int64_t idx = qsync_.acquire_ref();
if (OB_UNLIKELY(0 != ATOMIC_LOAD(&write_flag_))) {
qsync_.release_ref(idx);
ret = OB_EAGAIN;
// 第二次检查写锁状态(双重检查)
} else {
// success, do nothing
}
}
return ret;
}
#define ATOMIC_LOAD(x) __atomic_load_n((x), __ATOMIC_SEQ_CST)
#define ATOMIC_LOAD_ACQ(x) __atomic_load_n((x), __ATOMIC_ACQUIRE)
#define ATOMIC_LOAD_RLX(x) __atomic_load_n((x), __ATOMIC_RELAXED
ObQSyncLock的核心设计是将读锁引用计数分散到多个槽位:
高频原子操作:
吞吐量影响:
按线程ID分配:
还是不明白?
缓存伪共享是多核处理器系统中的一个常见性能瓶颈:
CACHE_ALIGNED是一个编译器指令,用于确保变量按缓存行大小对齐
struct Ref { Ref(): ref_(0) {} ~Ref() {} int64_t ref_ CACHE_ALIGNED; // 确保每个计数器独占缓存行 };
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。