关于多核 CPU 自旋锁 (spinlock) 的优化

本文探讨了自旋锁在多核CPU环境下的优化策略,包括MCSspinlock、CriticalSectionIntegration、NUMAAwareSpinlock等方法,以及MutexSchedulepercore和针对NUMA的调整。通过对自旋锁机制的深入研究,提出了一系列减少数据迁移和提高并发性能的技术方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

CPU的总线为铜薄膜,虽然摩尔定律使单位面积晶体管的密度不断增加,但是对于连接导线的电阻却没有明显的下降,导线的RC延迟几乎决定现有CPU性能,因此我们会看到传输数据在CPU的角度来看是个极为沉重的负担。我们看到intel 为了引入更多的CPU核心,从Skylake开始芯片总线由上一代的 ring-bus 转变为 2D-mesh, 虽然2D-mesh为数据提供了更多的迁移路径减少了数据堵塞,但也同样为数据一致性带来更多问题,例如过去ring-bus 结构下对于存在于某个CPU私用缓存的数据争抢请求只有两个方向(左和右), 但是在2D-mesh环境下会来自于4个方向(上,下,左,右),同时大家不久会看到更多CPU socket的服务器将会出现,为了优化现有的和将来会出现的自旋锁问题,我们开展了自旋锁的优化工作,在代码中具体包含了以下优化方法:

  1. MCS spinlock
    MCS 优化原理( 最新的Linux Kernel也采用了MCS)能够减少锁在不同CPU核心之间的迁移

具体参照:http://www.cise.ufl.edu/tr/DOC/REP-1992-71.pdf

  1. Critical Section Integration (CSI)
    本质上自旋锁产生的效果就是一个CPU core 按顺序逐一执行关键区域的代码,所以在我们的优化代码中将关键区域的代码以函数的形式表现出来,当线程抢锁的时候,如果发现有冲突,那么就将自己的函数挂在锁拥有者的队列上,然后使用MCS进入spinning 状态,而锁拥有者在执行完自己的关键区域之后,会检测是否还有其他锁的请求,如果有那么依次执行并且通知申请者,然后返回。可以看到通过这个方法所有的共享数据更新都是在CPU私用缓存内完成,能够大幅度减少共享数据的迁移,由于减少了迁移时间,那么加快了关键区域运行时间最终也减少了冲突可能性。

具体参照:https://users.ece.cmu.edu/~omutlu/pub/acs_asplos09.pdf

  1. NUMA Aware Spinlock (NAS)
    当前服务器多CPU socket (2/4/8) 给我们提供了更好的性能/功耗比值,但由于CPU片外的数据传输非常昂贵(慢),也引入了更加复杂的一致性需求。 所以我们引入了分布式一致性机制来减少锁的缓存行在不同CPU socket 之间的迁移,最终加速性能。

具体可以参照:https://www.usenix.org/system/files/conference/atc17/atc17-kashyap.pdf

  1. Mutex Schedule per core
    当线程数大于CPU核数的时候, 在自旋锁的场景下由于操作系统的CFS调度,那些没有抢到锁的线程

或者刚刚开始抢锁的线程会不断争抢锁的拥有者和已经处于spinning 状态线程的CPU资源。为了解决
这个问题我们为每一个CPU core 引入mutex, 这样在任何一个阶段只有一个线程可以进入spinning 状
态或者成为锁的拥有者。由于mutex 锁总是存在CPU core 的私用缓存,另外第一个获取mutex锁的线
程数据不会引入任何的系统调用,最终解决了OS带来的问题,数据表明该方法大大提升了整体性能。

  1. Optimization when NUMA is ON or OFF
    虽然NUMA ON可以减少程序访问内存的延迟,但是如果有些程序需要更大的内存带宽,NUMA OFF也是很好的选择。因此在本次优化中我们的代码同时考虑了 NUMA ON 和 NUMA OFF

以下是具体提升数据(与GLIBC pthread_spinlock 对比):
23_34_04__11_26_2018.jpg

使用同样的测试代码,由上图2个CPU socket 变为4个CPU socket(即使是NUMA off), 我们看到了又再次动态的提升了一倍的比例(由96核心的15倍变为192核心的30倍),说明我们的优化持续有效减少了数据和锁的迁移,最终提升性能。
图片.png

最新代码:http://gitlab.alipay-inc.com/eff/numa-spinlock

在进行软件优化的同时,我们看到仍然存在不必要的数据迁移,而这些问题必须有对应CPU硬件指令,为了彻底解决自旋锁问题,我们已经提出了CPU优化方案, 通过该方案可以达到自旋锁的理论值。

虽然性能得到提升,但是我们希望着重说明,如果能够避免使用这个技术,即避免产生由于同步导致的数据传输,才是最佳方案。举例说明:如果一个任务可以并行化并且我们有64个CPU核心, 但他们之间只有1%的串行化代码(如即使性能达到理论值的Spinlock操作), 那么根据 阿姆达尔法则,我们的吞吐量提升度是: T/(0.99T/64 + 0.01T) = 1/(0.99/64 + 0.01) = 39.26, 也就是说64个核心只给了我们40个核心的吞吐量,因此spinlock会严重影响吞吐量。越来越多的经验提醒我们,我们学习技术的目的也许是为了理解事物的规律,从而避免使用这些技术,例如如果使用好的设计从而避免产生同步,不使用spinlock的吞吐量一定是最大的,对么?

<think>我们被要求详细解释自旋锁和互斥锁的核心区别以及使用场景分析。根据引用资料,我们可以从等待机制、实现原理、性能特点、适用场景等方面进行对比。###1.等待机制-**自旋锁(Spinlock)**采用忙等待(Busy-Wait)策略:线程在尝试获取锁失败时,不会放弃CPU,而是通过循环不断检查锁状态,直到锁可用[^2][^4]。例如:```cwhile(lock_is_held);//循环等待acquire_lock();```-**互斥锁(Mutex)**采用阻塞等待策略:线程获取锁失败时,会主动让出CPU,进入阻塞状态(睡眠),由操作系统调度唤醒[^2][^4]。例如:```cif(!try_lock()){sleep();//放弃CPU,进入阻塞队列}```###2.实现原理-**自旋锁**-依赖底层原子操作(如CAS、Test-And-Set)实现[^4]。-在用户态即可完成锁操作,无需内核介入(轻量级)[^4]。-在单核CPU中需配合中断禁用(防止死锁)[^5]:```c#definespin_lock(lock)\do{\preempt_disable();//禁用内核抢占\__acquire(lock);//原子操作获取锁\}while(0)```-**互斥锁**-依赖操作系统内核的阻塞/唤醒机制(如信号量、条件变量)[^4]。-涉及用户态与内核态切换,开销较大[^2][^4]。###3.性能特点对比|**特性**|**自旋锁**|**互斥锁**||------------------|--------------------------------------------|--------------------------------------------||**CPU占用**|等待时持续占用CPU(空转)[^2]|等待时不占用CPU(线程睡眠)[^2]||**上下文切换**|无切换开销[^4]|有切换开销(约数微秒)[^4]||**适用锁时长**|极短(纳秒级)[^3][^4]|较长(微秒级以上)[^3][^4]||**单核适用性**|需禁用中断,否则死锁[^5]|直接可用|###4.核心区别总结|**维度**|**自旋锁**|**互斥锁**||------------------|--------------------------------------------|--------------------------------------------||**等待行为**|主动轮询|被动休眠||**实现层级**|用户态/原子指令|内核调度器||**开销来源**|CPU空转|上下文切换||**中断环境**|唯一选择(不可睡眠)[^3]|禁止使用(可能睡眠)|###5.使用场景分析####优先使用自旋锁的场景:1.**极短临界区**(如计数器递增)[^3][^4]-锁持有时间短于线程切换开销(通常<1μs)。2.**中断上下文**(如Linux内核中断处理)[^3]-中断处理中禁止睡眠,只能自旋。3.**实时性要求高**的系统[^4]-避免不可预测的调度延迟。4.**多核CPU环境**[^4]-单核自旋锁可能永久阻塞(除非禁用中断)。####优先使用互斥锁的场景:1.**较长临界区**(如文件I/O、复杂计算)[^3][^4]-避免长时间CPU空转浪费资源。2.**用户态应用程序**[^4]-开发简单,避免忙等待导致系统负载过高。3.**单核CPU环境**[^4]-自旋锁在单核无意义(除非配合中断禁用)。4.**需处理优先级反转的场景**-互斥锁支持优先级继承(如RTOS)[^4]。###6.典型错误示例-**在中断中误用互斥锁**(导致内核崩溃):```cvoidinterrupt_handler(){mutex_lock(&lock);//错误!中断上下文不可睡眠//...mutex_unlock(&lock);}```-**长临界区用自旋锁**(CPU利用率100%空转):```cspin_lock(&lock);sleep(10);//模拟长操作→灾难性后果spin_unlock(&lock);```###7.混合策略:自适应锁现代系统(如Linux`pthread_mutex`)提供自适应锁:-先自旋一段时间,若超时仍未获锁则转为阻塞[^4]。-平衡短临界区性能和长等待资源消耗。>**核心结论**:选择锁的关键在于评估**临界区执行时间**和**运行环境**(中断/多核/实时性)[^3][^4]。§§相关问题§§1.为什么在单核CPU上使用自旋锁时必须禁用中断?2.如何测量临界区的执行时间以选择合适的锁?3.互斥锁的优先级继承机制是如何解决优先级反转问题的?4.在用户态编程中,如何实现高效的自旋锁?</think>### 自旋锁与互斥锁的核心区别分析 #### 1. 等待机制(核心区别) - **自旋锁 (Spinlock)** 采用**忙等待 (Busy-Wait)** 策略: ```c while (lock_is_held); // 循环检查锁状态 acquire_lock(); // 锁释放后立即获取 ``` 线程持续占用CPU轮询锁状态,不释放CPU资源[^2][^4]。 - **互斥锁 (Mutex)** 采用**阻塞等待**策略: ```c if (!try_lock()) { sleep(); // 放弃CPU,进入阻塞队列 } ``` 线程主动让出CPU,由操作系统调度唤醒[^2][^4]。 #### 2. 实现原理与开销 | **特性** | **自旋锁** | **互斥锁** | |------------------|--------------------------------------------|--------------------------------------------| | **底层机制** | 原子指令(CAS/TAS)[^4] | 操作系统调度器(信号量/条件变量)[^4] | | **上下文切换** | 无切换(保持运行状态)[^2] | 需切换(休眠→唤醒,约数μs开销)[^2][^4] | | **CPU占用** | 等待时100%占用CPU核心[^1] | 等待时0%占用CPU[^1] | | **内核参与** | 纯用户态操作(轻量级)[^4] | 需内核态切换(重量级)[^4] | #### 3. 性能对比(纳秒级场景) ```mermaid graph LR A[锁请求] -->|自旋锁| B[循环检测锁状态] A -->|互斥锁| C[进入休眠状态] B -->|锁释放| D[立即获取锁] C -->|锁释放| E[等待唤醒+获取锁] D --> F[执行临界区] E --> F ``` - **自旋锁优势**:当锁持有时间 $t <$ 上下文切换时间 $t_c$(约1-2μs)时,性能显著更优[^4] - **互斥锁优势**:当 $t > t_c$ 时,避免CPU空转浪费资源[^4] #### 4. 关键特性对比 | **维度** | **自旋锁** | **互斥锁** | |------------------|--------------------------------------------|--------------------------------------------| | **中断安全性** | 可配合`spin_lock_irqsave()`禁用中断[^5] | 不可用于中断上下文[^3] | | **优先级反转** | 高风险(高优先级线程空转等待)[^4] | 支持优先级继承(如RTOS)[^4] | | **死锁风险** | 单核需禁用中断否则死锁[^5] | 自动检测死锁(高级实现) | | **内存占用** | 通常4-8字节 | 包含等待队列(约48字节) | #### 5. 使用场景分析 **优先使用自旋锁的场景**: 1. **极短临界区**(< 1μs操作,如计数器递增)[^3][^4] 2. **中断上下文**(中断处理函数不可睡眠)[^3] ```c void interrupt_handler() { spin_lock(&lock); // 唯一选择 // 更新硬件寄存器 spin_unlock(&lock); } ``` 3. **多核CPU+短操作**(避免切换开销)[^4] 4. **实时系统**(避免不可预测的调度延迟)[^4] **优先使用互斥锁的场景**: 1. **长临界区**(> 2μs操作,如文件I/O)[^3] 2. **用户态应用程序**(开发简单,避免忙等待)[^4] 3. **单核CPU环境**(自旋锁无意义)[^4] 4. **复杂同步需求**(如条件变量配合使用) #### 6. 混合策略:自适应锁 现代系统(如Linux `pthread_mutex`)提供**自适应锁**: 1. 先自旋一段时间(优化短操作) 2. 超时后转为阻塞(避免长等待浪费CPU)[^4] ```c pthread_mutex_t mutex = PTHREAD_MUTEX_ADAPTIVE_NP; ``` > **决策树**: > ```mermaid > graph TD > A[需要锁] --> B{在中断上下文?} > B -->|是| C[必须用自旋锁] > B -->|否| D{临界区执行时间} > D -->|t < 1μs| E[优先自旋锁] > D -->|t > 2μs| F[优先互斥锁] > D -->|1μs < t < 2μs| G[测试两种性能] > ``` ### 总结对比表 | **特性** | 自旋锁 | 互斥锁 | |------------------|------------------------|------------------------| | **等待方式** | 忙等待 | 阻塞休眠 | | **CPU占用** | 100% | 0% | | **开销来源** | CPU空转 | 上下文切换 | | **最佳场景** | 纳秒级操作 + 多核 | 微秒级操作 + 单核 | | **中断安全** | ✔️ 配合禁用中断 | ✖️ 禁止使用 | | **适用层级** | 内核/用户态 | 主要用户态 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值