看起来很鶸的Spinlock-深入理解Spinlock

看起来很鶸的Spinlock-深入理解Spinlock

  • Spinlock 自旋锁
    • 优缺点
    • 自旋锁的挑战
    • 汇编实现自旋锁
      • 汇编解读
        • 汇编的背景知识
        • 锁初始化
        • 加锁
        • 解锁
  • spinlock、semaphore和mutex的区别

Spinlock 自旋锁

在计算机科学中,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
     
  1. 这句是伪汇编,是锁的声明,声明一个锁的变量,当这个变量为1时即为锁被占有,0则为可获得(available)

加锁

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.
                             
  1. 上面这几行就是加锁的汇编了,
    2-1.首先将eax寄存器初始化为1;
    2-2.然后第二句的xchg实际上是一个交换值的指令,也就是将eax寄存器和锁变量的值交换,到这里应该都没什么问题;
    2-3.第三句可能稍微疑惑一下,不过注释也说的很清楚, test eax, eax 其实就是检测eax寄存器里到底是负数、零,还是正数。因为这一条指令得出的结果实际上就是eax+eax,所以如果eax是0,那么条件码ZF就会被置1;如果eax为1,我们就不能获得锁;
    2-4.第四句判断eax也就是锁变量的值是否为零,jnz实际上就是~ZF,对ZF求反,所以如果ZF没有被设置,ZF=0,那么eax=1,也就是锁已经被锁住了,所以我们需要在这里自旋来一直等待锁打开;
    2-5.第五句是当锁被获得后,返回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.
  1. 最后一部分是自旋锁打开的部分
    3-1.对eax寄存器做or操作,显然会使eax寄存器赋值为0
    3-2.交换eax和锁变量的值,也就是给锁变量赋值0,此时锁打开
    3-3.返回,此时锁已经被释放

是的,此时尼已经会用汇编实现自旋锁了

##自旋锁的重要优化

上面的简单实现适用于使用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、semaphore和mutex的区别

首先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则可以一个进程获得,另一个进程释放。

未完结。。。

你可能感兴趣的:(看起来很鶸的Spinlock-深入理解Spinlock)