自旋锁的实现
自旋锁的实现
MIPS中LL/SC指令介绍
在多线程程序中,为了实现对共享变量的互斥访问,一般都会用spinlock实现,而spinlock需要一个TestAndSet的原子操作。而这种原子操作是需要专门的硬件支持才能完成的,在MIPS中,是通过特殊的Load,Store操作LL(Load Linked,链接加载)以及SC(Store Conditional,条件存储)完成的。
LL 指令的功能是从内存中读取一个字
SC 指令的功能是向内存中写入一个字
LL/SC 指令的独特之处在于,它们不是一个简单的内存读取/写入的函数,当使用 LL 指令从内存中读取一个字之后,比如 LL d, off(b),处理器会记住 LL 指令的这次操作(会在 CPU 的寄存器中设置一个不可见的 bit 位),同时 LL 指令读取的地址 off(b) 也会保存在处理器的寄存器中。接下来的 SC 指令,比如 SC t, off(b),会检查上次 LL 指令执行后的 RMW 操作是否是原子操作(即不存在其它对这个地址的操作),如果是原子操作,则 t 的值将会被更新至内存中,同时 t 的值也会变为1,表示操作成功;反之,如果 RMW 的操作不是原子操作(即存在其它对这个地址的访问冲突),则 t 的值不会被更新至内存中,且 t 的值也会变为0,表示操作失败。
SC 指令执行失败的原因有两种:
- 在 LL/SC 操作序列的过程中,发生了一个异常(或中断),这些异常(或中断)可能会打乱 RMW 操作的原子性。
- 在多核处理器中,一个核在进行 RMW 操作时,别的核试图对同样的地址也进行操作,这会导致 SC 指令执行的失败。
简单的应用例子:
用MIPS LL/SC指令实现从内存单元100(R2)取数,把取出来的数加100并存回到100(R2)中的原子操作代码
1 | 1: LL R3,100(R2) |
CAS 原语
CAS (Compare-And-Swap) LL/SC这对CPU指令没有实现,但是实现了CAS. CAS是一组原语指令,用来实现多线程下的变量同步。 在 x86 下的指令CMPXCHG实现了CAS,前置LOCK既可以达到原子性操作。截止2013,大部分多核处理器均支持CAS。 CAS原语有三个参数,内存地址,期望值,新值。如果内存地址的值==期望值,表示该值未修改,此时可以修改成新值。否则表示修改失败,返回false,由用户决定后续操作。
简单自旋锁实现
什么是自旋锁
我们没有方法去控制对共享资源访问的有序性,但是我们有能力对共享资源采用锁的保护机制,当某个共享资源被锁住时,只有获取该锁的 CPU 核能够操作共享资源,其余试图访问共享资源的 CPU 核只能够等待这个锁的释放,这就是在 Linux 中被称为 Spinlock 的自旋锁保护机制。
Spinlock 的设计思想是基于一种被称为 Test-and-Set 的机制,它的操作分为三部分:
1)INIT
初始化 lock 值。示例如下:
1 | lock := CLEAR; |
2)LOCK
这个流程包括两部分:首先 CPU 核反复轮询 lock 值直到它处于空闲状态为止,然后利用 Test-and-Set 方式尝试设置该 lock 值,Test-and-Set 的操作可以描述成三步:
(a)读取 lock 值;
(b)设置 lock 值
(c)检查 lock 值是否设置成功,如果在步骤 (a) 之后还存在别的对 lock 值的操作,说明存在有并发访问 lock 值的情况,则步骤 (b) 的 lock 值将不能设置成功,还需要回到步骤 (a) 重新执行这个流程。
示例如下:
1 | while ((lock == BUSY) || (TestAndSet(lock) == BUSY)); |
(3)UNLOCK
释放 lock 值。示例如下:
1 | lock := CLEAR; |
MIPS实现(基于LL/SC原语实现)
传统的 C 语言是无法实现 Test-and-Set 的机制的,因为它无法在多核之间建立一个交互的机制,因此 Test-and-Set 需要处理器给以相应的支持。以 MIPS 为例,它提供了 LL(Load Linked Word) 和 SC(Store Conditional Word) 这两个汇编指令来实现对共享资源的保护。
简单的自旋锁
1 | Lock: |
1 | Unlock: |
基于排队的公平的自旋锁
简单自旋排队锁 设 a0 为锁的地址。按请求顺序来 service。
1 | Lock: (锁值为高低各 2 字节组成,高位为 tickets, 低位为 serve) |
解释:
LL
指令是 Load Linked,用于原子地将内存中的值加载到寄存器v0
。LI
指令是 Load Immediate,将立即数0x10000
加载到寄存器t0
。ADDU
指令是无符号整数相加。SC
指令是 Store Conditional,尝试将t0
存储回内存,如果存储成功,则返回 1,否则返回 0。BEQZ
指令是 Branch on Equal to Zero,如果t0
为零(存储失败),则跳转到标号1b
,即重新尝试获取锁。SRL
指令是 Shift Right Logical,将v0
寄存器右移 16 位,得到高位 tickets。LHU
指令是 Load Halfword Unsigned,将内存中的半字加载到寄存器t0
。BNE
指令是 Branch on Not Equal,如果t0
不等于v0
,则跳转到标号2b
,即循环等待,直到拿到锁。LHU
指令是 Load Halfword Unsigned,将内存中的半字加载到寄存器t0
。ADDIU
指令是 Add Immediate Unsigned,将t0
寄存器的值加 1。SH
指令是 Store Halfword,将t0
寄存器的值存储回内存。
CAS 实现自旋锁
CAS:指令定义 cas r1, r2, Mem;r1 存放期望值,r2 存放更新值,
1 | 获得锁://锁初始化为 0 |
CAS/ LL/SC原语
Compare_And_Swap(CAS)和 Load-Linked and Store-Conditional(LL/SC)是两种比较常见的 硬件同步原语。例如,Intel、AMD 的 x86 指令集和 Oracle/SUN 的 Sparc 指令集实现了 CAS指令;Alpha、PowerPC、MIPS、ARM 均实现了 LL/SC 指令。
(2)CAS 指令硬件实现比 LL/SC 的硬件实现复杂。使用 CAS 指令会碰到 ABA 问题,但是 LL/SC指令不会碰到该问题。LL/SC 指令中由于 SC 是尝试去写,因此在某些情况下,SC 执行成功 率很低,导致用 LL/SC 实现的锁执行开销变得很大。