在计算机科学中,Spinlock自旋锁多用于保持多线程同步,是一个简单的让线程不断循环( “loop” which means spin)来检查这个锁是否可用,通过忙等待来获得这个锁的锁,忙等在此处的意思是当前线程一直保持活跃(不睡眠)但是又不进行有用的任务而只是死循环,这样的锁的产生的效果就是忙等待。
一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。但是在一些实现中,自旋锁可能会被自动释放因为拥有锁的线程阻塞了,或者陷入睡眠。
自旋锁避免了操作系统进程调度和上下文切换的开销,因此对于线程只会阻塞很短时间的场合是有效的。因此操作系统的内核在很多地方往往用自旋锁。但是在自旋锁阻塞很长时间的时候它的损耗就很大,因为它在被调度的时候可能阻止了其他线程运行。一个线程被锁的时间越长,它在拥有锁的时候被操作系统调度打断的风险就越大。当这种情况发生时,其他的线程会陷入左旋(不断尝试去获得锁),然而拥有锁的线程因为没法进入运行状态而不会被调度,所以也不会释放锁。
显然,单核CPU不适于使用自旋锁,这里的单核CPU指的是单核单线程的CPU,因为,在同一时间只有一个线程是处在运行状态,假设运行线程A发现无法获取锁,只能等待解锁,但因为A自身不挂起,所以那个持有锁的线程B没有办法进入运行状态,只能等到操作系统分给A的时间片用完,才能有机会被调度。这种情况下使用自旋锁的代价很高。
当多个线程同时访问一个自旋锁的时候可能会导致竞争冒险,所以实现自旋锁还是十分具有挑战性的,因为编程人员必须将这种情况加以考虑。一般情况下,这种情况只有使用汇编语言指令才有可能实现,比如test-and-set等原子操作,而且不能被那种不支持真正的原子操作的编程语言锁所实现,保证读写操作必须是原子的。
而在没有这些操作的架构上,或者需要高级语言实现的,那就要采用一个非原子锁算法了,比如Peterson算法(这个os课上学过),然而,这样的一种实现就需要比自旋锁更多的内存,而且解锁后的运行也会更缓慢,而且如果乱序执行被允许的话高级语言还是没法实现自旋锁的。
获取、释放自旋锁,实际上是读写自旋锁的存储内存或寄存器。因此这种读写操作必须是原子的。通常用test-and-set等原子操作来实现。[1]
可运行于Intel 80386兼容处理器:
; Intel syntax
locked: ; The lock variable. 1 = locked, 0 = unlocked.
dd 0
spin_lock:
mov eax, 1 ; Set the EAX register to 1.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
; This will always store 1 to the lock, leaving
; the previous value in the EAX register.
test eax, eax ; Test EAX with itself. Among other things, this will
; set the processor's Zero Flag if EAX is 0.
; If EAX is 0, then the lock was unlocked and
; we just locked it.
; Otherwise, EAX is 1 and we didn't acquire the lock.
jnz spin_lock ; Jump back to the MOV instruction if the Zero Flag is
; not set; the lock was previously locked, and so
; we need to spin until it becomes unlocked.
ret ; The lock has been acquired, return to the calling
; function.
spin_unlock:
xor eax, eax ; Set the EAX register to 0.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
ret ; The lock has been released.
看第一遍没看懂,汗。。。 把csapp拿出来翻了一下才想起来。
先铺垫一点简单的知识,只要懂一点点计算机都能看得懂的那种应该,或者说是,复习
mov 数据传送指令,顾名思义就是传送字节的指令
以t=a+b为例 条件码说明如下:
CF 进位标志。(unsigned)a < (unsigned)b 无符号溢出
ZF 零标志。最近操作结果为0,即(t==0)
SF 符号标志。负数,即(t < 0)
OF 溢出标志。有符号溢出 (a<0 == b<0) && ( t<0 != a<0)
jnz (jne) ~ZF 不相等/非0⃣️
test命令对于操作数S2,S1实际操作是S1 & S2
下面开始解释上面短短几行汇编(我可真是啰嗦而浅薄啊呜呜):
locked: ; The lock variable. 1 = locked, 0 = unlocked.
dd 0
spin_lock:
mov eax, 1 ; Set the EAX register to 1.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
; This will always store 1 to the lock, leaving
; the previous value in the EAX register.
test eax, eax ; Test EAX with itself. Among other things, this will
; set the processor's Zero Flag if EAX is 0.
; If EAX is 0, then the lock was unlocked and
; we just locked it.
; Otherwise, EAX is 1 and we didn't acquire the lock.
jnz spin_lock ; Jump back to the MOV instruction if the Zero Flag is
; not set; the lock was previously locked, and so
; we need to spin until it becomes unlocked.
ret ; The lock has been acquired, return to the calling
; function.
spin_unlock:
xor eax, eax ; Set the EAX register to 0.
xchg eax, [locked] ; Atomically swap the EAX register with
; the lock variable.
ret ; The lock has been released.
是的,此时尼已经会用汇编实现自旋锁了
##自旋锁的重要优化
上面的简单实现适用于使用x86架构的所有CPU。但是,可以进行许多性能优化:
在后来的x86架构实现中,spin_unlock可以安全地使用解锁的MOV指令而不是速度较慢的加锁XCHG指令。这是由于微妙的内存排序规则支持这一点,即使MOV不是一个完整的内存屏障。但是,某些处理器(一些Cyrix处理器,一些Intel Pentium Pro修订版(由于错误)以及早期的Pentium和i486 SMP系统)会出错,受锁保护的数据可能会被破坏。在大多数非x86体系结构中,必须使用显式内存屏障或原子指令(如示例中所示)。在某些系统上,例如IA-64,有一些特殊的“解锁”指令可以提供所需的内存排序。
内存屏障(Memory barrier),是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
为了减少CPU间的总线流量,尝试获取锁的代码应该循环读取而不尝试写任何东西,直到它读取更改的值。由于MESI缓存协议,这导致锁的缓存行变为“共享”;然后,当CPU等待锁定时,总是没有总线流量。这种优化对于每个CPU都有缓存的所有CPU架构都有效,因为MESI非常普遍。
MESI协议是一个基于失效的缓存一致性协议,UIUC牛逼
首先spinlock是只有在内核态才有的,当然你也可以在用户态自己实现,但是如果想要调用spinlock_t类型,那只有内核态才有。但是semaphore是内核态和用户态都有的,mutex是一种特殊的semaphore。
spinlock是一种忙等待,也就是说,进程是不会睡眠的,只是一直在那里死循环。而mutex是睡等,也就是说,如果拿不到临界资源,那它会选择进程睡眠。那什么时候用spinlock,什么时候用mutex呢?首先,如果是在不允许睡眠的情况下,只能只用spinlock,比如中断的时候。然后如果临界区中执行代码的时间小于进程上下文切换的时间,那应该使用spinlock。反之应该使用mutex。
那mutex和semaphore有什么区别呢?mutex是用作互斥的,而semaphore是用作同步的。也就是说,mutex的初始化一定是为1,而semaphore可以是任意的数,所以如果使用mutex,那第一个进入临界区的进程一定可以执行,而其他的进程必须等待。而semaphore则不一定,如果一开始初始化为0,则所有进程都必须等待。同时mutex和semaphore还有一个区别是,获得mutex的进程必须亲自释放它,而semaphore则可以一个进程获得,另一个进程释放。
未完结。。。